mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SSE: Fix math expression to support NoData results (#61721)
* update perFloat to support NoData * update union to correctly handle no-data
This commit is contained in:
parent
d0cf7ff8e9
commit
c0913ce718
@ -7,6 +7,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/expr/mathexp/parse"
|
"github.com/grafana/grafana/pkg/expr/mathexp/parse"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -201,6 +202,11 @@ func union(aResults, bResults Results) []*Union {
|
|||||||
}
|
}
|
||||||
if aValueLen == 1 || bValueLen == 1 {
|
if aValueLen == 1 || bValueLen == 1 {
|
||||||
if aResults.Values[0].Type() == parse.TypeNoData || bResults.Values[0].Type() == parse.TypeNoData {
|
if aResults.Values[0].Type() == parse.TypeNoData || bResults.Values[0].Type() == parse.TypeNoData {
|
||||||
|
unions = append(unions, &Union{
|
||||||
|
Labels: nil,
|
||||||
|
A: aResults.Values[0],
|
||||||
|
B: bResults.Values[0],
|
||||||
|
})
|
||||||
return unions
|
return unions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -285,6 +291,8 @@ func (e *State) walkBinary(node *parse.BinaryNode) (Results, error) {
|
|||||||
// Scalar op Series
|
// Scalar op Series
|
||||||
case Series:
|
case Series:
|
||||||
value, err = e.biSeriesNumber(uni.Labels, node.OpStr, bt, aFloat, false)
|
value, err = e.biSeriesNumber(uni.Labels, node.OpStr, bt, aFloat, false)
|
||||||
|
case NoData:
|
||||||
|
value = uni.B
|
||||||
default:
|
default:
|
||||||
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
|
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
|
||||||
}
|
}
|
||||||
@ -301,6 +309,8 @@ func (e *State) walkBinary(node *parse.BinaryNode) (Results, error) {
|
|||||||
// case Series op Series
|
// case Series op Series
|
||||||
case Series:
|
case Series:
|
||||||
value, err = e.biSeriesSeries(uni.Labels, node.OpStr, at, bt)
|
value, err = e.biSeriesSeries(uni.Labels, node.OpStr, at, bt)
|
||||||
|
case NoData:
|
||||||
|
value = uni.B
|
||||||
default:
|
default:
|
||||||
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
|
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
|
||||||
}
|
}
|
||||||
@ -315,9 +325,13 @@ func (e *State) walkBinary(node *parse.BinaryNode) (Results, error) {
|
|||||||
value, err = e.biScalarNumber(uni.Labels, node.OpStr, at, bFloat, true)
|
value, err = e.biScalarNumber(uni.Labels, node.OpStr, at, bFloat, true)
|
||||||
case Series:
|
case Series:
|
||||||
value, err = e.biSeriesNumber(uni.Labels, node.OpStr, bt, aFloat, false)
|
value, err = e.biSeriesNumber(uni.Labels, node.OpStr, bt, aFloat, false)
|
||||||
|
case NoData:
|
||||||
|
value = uni.B
|
||||||
default:
|
default:
|
||||||
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
|
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
|
||||||
}
|
}
|
||||||
|
case NoData:
|
||||||
|
value = uni.A
|
||||||
default:
|
default:
|
||||||
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
|
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package mathexp
|
package mathexp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -8,6 +9,7 @@ import (
|
|||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNaN(t *testing.T) {
|
func TestNaN(t *testing.T) {
|
||||||
@ -422,3 +424,89 @@ func TestNullValues(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNoData(t *testing.T) {
|
||||||
|
t.Run("unary operation return NoData if input NoData", func(t *testing.T) {
|
||||||
|
unaryOps := []string{
|
||||||
|
"abs($A)",
|
||||||
|
"is_inf($A)",
|
||||||
|
"is_nan($A)",
|
||||||
|
"is_null($A)",
|
||||||
|
"is_number($A)",
|
||||||
|
"log($A)",
|
||||||
|
"round($A)",
|
||||||
|
"ceil($A)",
|
||||||
|
"floor($A)",
|
||||||
|
"!$A",
|
||||||
|
"-$A",
|
||||||
|
}
|
||||||
|
vars := Vars{"A": Results{[]Value{NewNoData()}}}
|
||||||
|
for _, expr := range unaryOps {
|
||||||
|
t.Run(fmt.Sprintf("op: %s", expr), func(t *testing.T) {
|
||||||
|
e, err := New(expr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if e != nil {
|
||||||
|
res, err := e.Execute("", vars)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, res.Values, 1)
|
||||||
|
require.Equal(t, NewNoData(), res.Values[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
makeVars := func(a, b Value) Vars {
|
||||||
|
return Vars{
|
||||||
|
"A": Results{[]Value{a}},
|
||||||
|
"B": Results{[]Value{b}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bin_ops := []string{
|
||||||
|
"$A || $B",
|
||||||
|
"$A && $B",
|
||||||
|
"$A + $B",
|
||||||
|
"$A * $B",
|
||||||
|
"$A - $B",
|
||||||
|
"$A / $B",
|
||||||
|
"$A ** $B",
|
||||||
|
"$A % $B",
|
||||||
|
"$A == $B",
|
||||||
|
"$A > $B",
|
||||||
|
"$A != $B",
|
||||||
|
"$A < $B",
|
||||||
|
"$A >= $B",
|
||||||
|
"$A <= $B",
|
||||||
|
"$A || $B",
|
||||||
|
"$A && $B",
|
||||||
|
}
|
||||||
|
series := makeSeries("test", nil, tp{time.Unix(5, 0), float64Pointer(2)})
|
||||||
|
for _, expr := range bin_ops {
|
||||||
|
t.Run(fmt.Sprintf("op: %s", expr), func(t *testing.T) {
|
||||||
|
e, err := New(expr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if e != nil {
|
||||||
|
t.Run("$A,$B=nodata", func(t *testing.T) {
|
||||||
|
res, err := e.Execute("", makeVars(NewNoData(), NewNoData()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, res.Values, 1)
|
||||||
|
require.Equal(t, NewNoData(), res.Values[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("$A=nodata, $B=series", func(t *testing.T) {
|
||||||
|
res, err := e.Execute("", makeVars(NewNoData(), series))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, res.Values, 1)
|
||||||
|
require.Equal(t, NewNoData(), res.Values[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("$A=series, $B=nodata", func(t *testing.T) {
|
||||||
|
res, err := e.Execute("", makeVars(NewNoData(), series))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, res.Values, 1)
|
||||||
|
require.Equal(t, NewNoData(), res.Values[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -229,6 +229,8 @@ func perFloat(e *State, val Value, floatF func(x float64) float64) (Value, error
|
|||||||
newSeries.SetPoint(i, t, &nF)
|
newSeries.SetPoint(i, t, &nF)
|
||||||
}
|
}
|
||||||
newVal = newSeries
|
newVal = newSeries
|
||||||
|
case parse.TypeNoData:
|
||||||
|
newVal = NewNoData()
|
||||||
default:
|
default:
|
||||||
// TODO: Should we deal with TypeString, TypeVariantSet?
|
// TODO: Should we deal with TypeString, TypeVariantSet?
|
||||||
}
|
}
|
||||||
@ -258,6 +260,8 @@ func perNullableFloat(e *State, val Value, floatF func(x *float64) *float64) (Va
|
|||||||
newSeries.SetPoint(i, t, floatF(f))
|
newSeries.SetPoint(i, t, floatF(f))
|
||||||
}
|
}
|
||||||
newVal = newSeries
|
newVal = newSeries
|
||||||
|
case parse.TypeNoData:
|
||||||
|
newVal = NewNoData()
|
||||||
default:
|
default:
|
||||||
// TODO: Should we deal with TypeString, TypeVariantSet?
|
// TODO: Should we deal with TypeString, TypeVariantSet?
|
||||||
}
|
}
|
||||||
|
@ -212,5 +212,9 @@ func (s NoData) AddNotice(notice data.Notice) {
|
|||||||
func (s NoData) AsDataFrame() *data.Frame { return s.Frame }
|
func (s NoData) AsDataFrame() *data.Frame { return s.Frame }
|
||||||
|
|
||||||
func (s NoData) New() NoData {
|
func (s NoData) New() NoData {
|
||||||
|
return NewNoData()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNoData() NoData {
|
||||||
return NoData{data.NewFrame("no data")}
|
return NoData{data.NewFrame("no data")}
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,13 @@ func Test_union(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
unionsAre: assert.EqualValues,
|
unionsAre: assert.EqualValues,
|
||||||
unions: []*Union{},
|
unions: []*Union{
|
||||||
|
{
|
||||||
|
Labels: nil,
|
||||||
|
A: makeSeries("a", data.Labels{"id": "1"}),
|
||||||
|
B: NewNoData(),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "incompatible tags of different length with will result in no unions when len(A) != 1 && len(B) != 1",
|
name: "incompatible tags of different length with will result in no unions when len(A) != 1 && len(B) != 1",
|
||||||
@ -260,6 +266,30 @@ func Test_union(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "A is no-data and B is anything makes no-data",
|
||||||
|
// Is this the behavior we want? A result within the results will no longer
|
||||||
|
// be uniquely identifiable.
|
||||||
|
aResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
NewNoData(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeries("a", data.Labels{"id": "1"}),
|
||||||
|
makeSeries("aa", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unionsAre: assert.EqualValues,
|
||||||
|
unions: []*Union{
|
||||||
|
{
|
||||||
|
Labels: nil,
|
||||||
|
A: NewNoData(),
|
||||||
|
B: makeSeries("a", data.Labels{"id": "1"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
Loading…
Reference in New Issue
Block a user