mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
* Add operators IN, NOT IN, MATCH ANY, and MATCH NONE
* Implement Static.append()
* Print string arrays with '`' instead of '"'
Example ["aa", "bb"] -> [`aa`, `bb`]
* Rewrite binary operations to array equivalent
The rewriter handles the following cases:
- { .a = "a" || .a = "b" } => { .a IN ["a", "b"] }
- { .a != "a" && .a != "b" } => { .a NOT IN ["a", "b"] }
- { .a =~ "a" || .a =~ "b" } => { .a MATCH ANY ["a", "b"] }
- { .a !~ "a" && .a !~ "b" } => { .a MATCH NONE ["a", "b"] }
* Add test cases with arrays to TestBinOp
* traceql.Parse() applies AST optimizations by default
* Rename predicate_test.go to predicates_test.go
* Add array predicates to parquetquery
* BREAKING CHANGE: more precise regex validation
BinaryOperation.validate() considered everythin a valid regex where
FieldExpression.EncodeToString(false) happened to return a valid regex.
With this change the RHS of a regex operation is required to be a string
or string array
* vp4: handle predicates with arrays correctly
* vp5: handle predicates with arrays correctly
* vp3: handle predicates with arrays correctly
* Add test cases for optimized queries
* Update query test examples with optimizable queries
* vp3: fix handling of array types
* CHANGELOG.md
* Pass array operators down to fetch layer
* Read to first virtual row after seeking on a low definition level
This helps avoid additional spans/op in queries with only
resource attributes
---------
Co-authored-by: Martin Disibio <[email protected]>
1221 lines
35 KiB
Go
1221 lines
35 KiB
Go
package traceql
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestRootExprIsNoop(t *testing.T) {
|
|
noops := []string{
|
|
"{false}",
|
|
"{1=0}",
|
|
"({false})",
|
|
"{false} && {false}",
|
|
"{false} >> {false}",
|
|
}
|
|
|
|
for _, q := range noops {
|
|
expr, err := Parse(q)
|
|
require.NoError(t, err)
|
|
require.True(t, expr.IsNoop(), "Query should be noop: %v", q)
|
|
}
|
|
|
|
ops := []string{
|
|
"{true}",
|
|
"{1=1}",
|
|
"{.foo = 1}",
|
|
"{false} || {true}",
|
|
|
|
// This is a noop but not yet detected
|
|
"{false} && {true}",
|
|
}
|
|
|
|
for _, q := range ops {
|
|
expr, err := Parse(q)
|
|
require.NoError(t, err)
|
|
require.False(t, expr.IsNoop(), "Query should not be noop: %v", q)
|
|
}
|
|
}
|
|
|
|
func TestNewStaticNil(t *testing.T) {
|
|
s := NewStaticNil()
|
|
assert.Equal(t, TypeNil, s.Type)
|
|
assert.Equal(t, Static{}, s)
|
|
}
|
|
|
|
func TestStatic_Int(t *testing.T) {
|
|
tests := []struct {
|
|
arg any
|
|
ok bool
|
|
}{
|
|
// supported values
|
|
{arg: -math.MaxInt, ok: true},
|
|
{arg: -1, ok: true},
|
|
{arg: 0, ok: true},
|
|
{arg: 1, ok: true},
|
|
{arg: math.MaxInt, ok: true},
|
|
// unsupported values
|
|
{arg: "test"},
|
|
{arg: 3.14},
|
|
{arg: true},
|
|
{arg: StatusOk},
|
|
{arg: KindClient},
|
|
{arg: []int{1}},
|
|
{arg: []float64{1.0}},
|
|
{arg: []bool{true}},
|
|
{arg: []string{"test"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(testName(tt.arg), func(t *testing.T) {
|
|
static := newStatic(tt.arg)
|
|
i, ok := static.Int()
|
|
|
|
require.Equal(t, tt.ok, ok)
|
|
if tt.ok {
|
|
assert.Equal(t, tt.arg, i)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_Float(t *testing.T) {
|
|
tests := []struct {
|
|
arg any
|
|
want float64
|
|
}{
|
|
{arg: -math.MaxFloat64},
|
|
{arg: -1.0},
|
|
{arg: 0.0},
|
|
{arg: 10.0 + math.SmallestNonzeroFloat64},
|
|
{arg: 1.0},
|
|
{arg: math.MaxFloat64},
|
|
{arg: int(3117), want: 3117.0},
|
|
{arg: time.Duration(101), want: 101.0},
|
|
{arg: "test", want: math.NaN()},
|
|
{arg: true, want: math.NaN()},
|
|
{arg: StatusError, want: math.NaN()},
|
|
{arg: KindServer, want: math.NaN()},
|
|
{arg: []int{1}, want: math.NaN()},
|
|
{arg: []float64{1.0}, want: math.NaN()},
|
|
{arg: []bool{true}, want: math.NaN()},
|
|
{arg: []string{"test"}, want: math.NaN()},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(testName(tt.arg), func(t *testing.T) {
|
|
static := newStatic(tt.arg)
|
|
f := static.Float()
|
|
|
|
if static.Type == TypeFloat {
|
|
assert.Equal(t, tt.arg, f)
|
|
} else if math.IsNaN(tt.want) {
|
|
assert.True(t, math.IsNaN(f))
|
|
} else {
|
|
assert.Equal(t, tt.want, f)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_String(t *testing.T) {
|
|
tests := []struct {
|
|
arg any
|
|
want string
|
|
}{
|
|
{arg: nil, want: "nil"},
|
|
{arg: 101, want: "101"},
|
|
{arg: -10, want: "-10"},
|
|
{arg: -1.0, want: "-1.0"},
|
|
{arg: 0.0, want: "0.0"},
|
|
{arg: 10.0, want: "10.0"},
|
|
{arg: 12.34, want: "12.34"},
|
|
{arg: 1.23e+23, want: "1.23e+23"},
|
|
{arg: 12.34e+2, want: "1234.0"},
|
|
{arg: "test", want: "`test`"},
|
|
{arg: true, want: "true"},
|
|
{arg: StatusOk, want: "ok"},
|
|
{arg: KindClient, want: "client"},
|
|
{arg: time.Duration(70) * time.Second, want: "1m10s"},
|
|
{arg: []int{1, 2, 3}, want: "[1, 2, 3]"},
|
|
{arg: []float64{1.1, 3.3}, want: "[1.1, 3.3]"},
|
|
{arg: []bool{true, true, false}, want: "[true, true, false]"},
|
|
{arg: []string{"aa", "bb"}, want: "[`aa`, `bb`]"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(testName(tt.arg), func(t *testing.T) {
|
|
static := newStatic(tt.arg)
|
|
assert.Equal(t, tt.want, static.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_Bool(t *testing.T) {
|
|
tests := []struct {
|
|
arg any
|
|
ok bool
|
|
}{
|
|
// supported values
|
|
{arg: false, ok: true},
|
|
{arg: true, ok: true},
|
|
// unsupported values
|
|
{arg: 3.14},
|
|
{arg: "test"},
|
|
{arg: time.Duration(1)},
|
|
{arg: StatusOk},
|
|
{arg: KindClient},
|
|
{arg: []int{1, 2, 3}},
|
|
{arg: []float64{1.0, 2.2}},
|
|
{arg: []bool{true, true}},
|
|
{arg: []string{"aa", "bb"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(testName(tt.arg), func(t *testing.T) {
|
|
static := newStatic(tt.arg)
|
|
b, ok := static.Bool()
|
|
|
|
require.Equal(t, tt.ok, ok)
|
|
if tt.ok {
|
|
assert.Equal(t, tt.arg, b)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_Duration(t *testing.T) {
|
|
tests := []struct {
|
|
arg any
|
|
ok bool
|
|
}{
|
|
// supported values
|
|
{arg: time.Duration(0), ok: true},
|
|
{arg: time.Duration(1), ok: true},
|
|
{arg: time.Duration(100) * time.Second, ok: true},
|
|
// unsupported values
|
|
{arg: 1},
|
|
{arg: 3.14},
|
|
{arg: "test"},
|
|
{arg: true},
|
|
{arg: StatusOk},
|
|
{arg: KindClient},
|
|
{arg: []int{1, 2, 3}},
|
|
{arg: []float64{1.0, 2.2}},
|
|
{arg: []bool{true, true}},
|
|
{arg: []string{"aa", "bb"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(testName(tt.arg), func(t *testing.T) {
|
|
static := newStatic(tt.arg)
|
|
d, ok := static.Duration()
|
|
|
|
require.Equal(t, tt.ok, ok)
|
|
if tt.ok {
|
|
assert.Equal(t, tt.arg, d)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_Status(t *testing.T) {
|
|
tests := []struct {
|
|
arg any
|
|
ok bool
|
|
}{
|
|
// supported values
|
|
{arg: StatusError, ok: true},
|
|
{arg: StatusOk, ok: true},
|
|
{arg: StatusUnset, ok: true},
|
|
// unsupported values
|
|
{arg: 1},
|
|
{arg: 3.14},
|
|
{arg: "test"},
|
|
{arg: true},
|
|
{arg: KindClient},
|
|
{arg: []int{1, 2, 3}},
|
|
{arg: []float64{1.0, 2.2}},
|
|
{arg: []bool{true, true}},
|
|
{arg: []string{"aa", "bb"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(testName(tt.arg), func(t *testing.T) {
|
|
static := newStatic(tt.arg)
|
|
s, ok := static.Status()
|
|
|
|
require.Equal(t, tt.ok, ok)
|
|
if tt.ok {
|
|
assert.Equal(t, tt.arg, s)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_Kind(t *testing.T) {
|
|
tests := []struct {
|
|
arg any
|
|
ok bool
|
|
}{
|
|
// supported values
|
|
{arg: KindUnspecified, ok: true},
|
|
{arg: KindInternal, ok: true},
|
|
{arg: KindClient, ok: true},
|
|
{arg: KindServer, ok: true},
|
|
{arg: KindProducer, ok: true},
|
|
{arg: KindConsumer, ok: true},
|
|
// unsupported values
|
|
{arg: 1},
|
|
{arg: 3.14},
|
|
{arg: "test"},
|
|
{arg: true},
|
|
{arg: StatusOk},
|
|
{arg: []int{1, 2, 3}},
|
|
{arg: []float64{1.0, 2.2}},
|
|
{arg: []bool{true, true}},
|
|
{arg: []string{"aa", "bb"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(testName(tt.arg), func(t *testing.T) {
|
|
static := newStatic(tt.arg)
|
|
k, ok := static.Kind()
|
|
|
|
require.Equal(t, tt.ok, ok)
|
|
if tt.ok {
|
|
assert.Equal(t, tt.arg, k)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_IntArray(t *testing.T) {
|
|
tests := []struct {
|
|
arg any
|
|
ok bool
|
|
}{
|
|
// supported values
|
|
{arg: []int(nil), ok: true},
|
|
{arg: []int{}, ok: true},
|
|
{arg: []int{1}, ok: true},
|
|
{arg: []int{1, 2, 3, 4, 5}, ok: true},
|
|
// unsupported values
|
|
{arg: 1},
|
|
{arg: 3.14},
|
|
{arg: "test"},
|
|
{arg: true},
|
|
{arg: StatusOk},
|
|
{arg: KindClient},
|
|
{arg: []float64{1.0, 2.2}},
|
|
{arg: []bool{true, true}},
|
|
{arg: []string{"aa", "bb"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(testName(tt.arg), func(t *testing.T) {
|
|
static := newStatic(tt.arg)
|
|
a, ok := static.IntArray()
|
|
|
|
require.Equal(t, tt.ok, ok)
|
|
if tt.ok {
|
|
assert.Equal(t, tt.arg, a)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_FloatArray(t *testing.T) {
|
|
tests := []struct {
|
|
arg any
|
|
ok bool
|
|
}{
|
|
// supported values
|
|
{arg: []float64(nil), ok: true},
|
|
{arg: []float64{}, ok: true},
|
|
{arg: []float64{1.11}, ok: true},
|
|
{arg: []float64{1.11 + math.SmallestNonzeroFloat64}, ok: true},
|
|
{arg: []float64{1.23, 2.0, 3.14, 4.1, 5.15}, ok: true},
|
|
// unsupported values
|
|
{arg: 1},
|
|
{arg: 3.14},
|
|
{arg: "test"},
|
|
{arg: true},
|
|
{arg: StatusOk},
|
|
{arg: KindClient},
|
|
{arg: []int{1, 2}},
|
|
{arg: []bool{true, true}},
|
|
{arg: []string{"aa", "bb"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(testName(tt.arg), func(t *testing.T) {
|
|
static := newStatic(tt.arg)
|
|
f, ok := static.FloatArray()
|
|
|
|
require.Equal(t, tt.ok, ok)
|
|
if tt.ok {
|
|
assert.Equal(t, tt.arg, f)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_StringArray(t *testing.T) {
|
|
tests := []struct {
|
|
arg any
|
|
ok bool
|
|
}{
|
|
// supported values
|
|
{arg: []string(nil), ok: true},
|
|
{arg: []string{}, ok: true},
|
|
{arg: []string{""}, ok: true},
|
|
{arg: []string{"aa"}, ok: true},
|
|
{arg: []string{"aa", "bb"}, ok: true},
|
|
// unsupported values
|
|
{arg: 1},
|
|
{arg: 3.14},
|
|
{arg: "test"},
|
|
{arg: true},
|
|
{arg: StatusOk},
|
|
{arg: KindClient},
|
|
{arg: []int{1, 2}},
|
|
{arg: []float64{1.0, 2.2}},
|
|
{arg: []bool{true, true}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(testName(tt.arg), func(t *testing.T) {
|
|
static := newStatic(tt.arg)
|
|
s, ok := static.StringArray()
|
|
|
|
require.Equal(t, tt.ok, ok)
|
|
if tt.ok {
|
|
assert.Equal(t, tt.arg, s)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_BooleanArray(t *testing.T) {
|
|
tests := []struct {
|
|
arg any
|
|
ok bool
|
|
}{
|
|
// supported values
|
|
{arg: []bool(nil), ok: true},
|
|
{arg: []bool{}, ok: true},
|
|
{arg: []bool{true}, ok: true},
|
|
{arg: []bool{false}, ok: true},
|
|
{arg: []bool{false, true}, ok: true},
|
|
// unsupported values
|
|
{arg: 1},
|
|
{arg: 3.14},
|
|
{arg: "test"},
|
|
{arg: true},
|
|
{arg: StatusOk},
|
|
{arg: KindClient},
|
|
{arg: []int{1, 2}},
|
|
{arg: []float64{1.0, 2.2}},
|
|
{arg: []string{"aa", "bb"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(testName(tt.arg), func(t *testing.T) {
|
|
static := newStatic(tt.arg)
|
|
b, ok := static.BooleanArray()
|
|
|
|
require.Equal(t, tt.ok, ok)
|
|
if tt.ok {
|
|
assert.Equal(t, tt.arg, b)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_MapKey(t *testing.T) {
|
|
staticVals := []Static{
|
|
NewStaticNil(),
|
|
NewStaticInt(0), NewStaticInt(1), NewStaticInt(2), NewStaticInt(-2), NewStaticInt(math.MaxInt), NewStaticInt(math.MinInt),
|
|
NewStaticDuration(0), NewStaticDuration(2), NewStaticDuration(-2),
|
|
NewStaticFloat(0), NewStaticFloat(2), NewStaticFloat(-2), NewStaticFloat(math.SmallestNonzeroFloat64), NewStaticFloat(math.MaxFloat64), NewStaticFloat(-math.MaxFloat64),
|
|
NewStaticKind(KindUnspecified), NewStaticKind(KindClient), // corresponds to 0 and 2
|
|
NewStaticStatus(StatusError), NewStaticStatus(StatusUnset), // corresponds to 0 and 2
|
|
NewStaticBool(false), NewStaticBool(true),
|
|
NewStaticString(""), NewStaticString("foo"),
|
|
NewStaticIntArray([]int{}), NewStaticIntArray([]int{1, 2, 3}),
|
|
NewStaticFloatArray([]float64{}), NewStaticFloatArray([]float64{0.0}), NewStaticFloatArray([]float64{0.0, 2.0, 3.0}),
|
|
NewStaticStringArray([]string{}), NewStaticStringArray([]string{""}), NewStaticStringArray([]string{"foo"}), NewStaticStringArray([]string{"f", "o", "o"}),
|
|
NewStaticBooleanArray([]bool{}), NewStaticBooleanArray([]bool{false}), NewStaticBooleanArray([]bool{true}),
|
|
}
|
|
|
|
// All above values must have unique MapKey
|
|
occurredValues := make(map[StaticMapKey]Static, len(staticVals))
|
|
for _, s := range staticVals {
|
|
mk := s.MapKey()
|
|
|
|
prev, found := occurredValues[mk]
|
|
occurredValues[mk] = s
|
|
|
|
assert.False(t, found, "static values produce the same MapKey %s, %s", prev.String(), s.String())
|
|
}
|
|
}
|
|
|
|
func BenchmarkStatic_MapKey(b *testing.B) {
|
|
testCases := []Static{
|
|
NewStaticStringArray([]string{"foo", "bar"}),
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
b.Run(tc.String(), func(b *testing.B) {
|
|
for b.Loop() {
|
|
_ = tc.MapKey()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_Equals(t *testing.T) {
|
|
areEqual := []struct {
|
|
lhs, rhs Static
|
|
}{
|
|
{NewStaticInt(1), NewStaticInt(1)},
|
|
{NewStaticFloat(1.5), NewStaticFloat(1.5)},
|
|
{NewStaticInt(2), NewStaticFloat(2.0)},
|
|
{NewStaticFloat(2.0), NewStaticInt(2)},
|
|
{NewStaticString("foo"), NewStaticString("foo")},
|
|
{NewStaticBool(true), NewStaticBool(true)},
|
|
{NewStaticBool(false), NewStaticBool(false)},
|
|
{NewStaticDuration(1 * time.Second), NewStaticDuration(1000 * time.Millisecond)},
|
|
{NewStaticDuration(0), NewStaticInt(0)},
|
|
{NewStaticInt(0), NewStaticDuration(0)},
|
|
{NewStaticStatus(StatusOk), NewStaticStatus(StatusOk)},
|
|
{NewStaticKind(KindClient), NewStaticKind(KindClient)},
|
|
{NewStaticIntArray([]int{}), NewStaticIntArray(nil)},
|
|
{NewStaticIntArray([]int{11, 111}), NewStaticIntArray([]int{11, 111})},
|
|
{NewStaticFloatArray([]float64{}), NewStaticFloatArray(nil)},
|
|
{NewStaticFloatArray([]float64{1.1, 2.2}), NewStaticFloatArray([]float64{1.1, 2.2})},
|
|
{NewStaticStringArray([]string{}), NewStaticStringArray(nil)},
|
|
{NewStaticStringArray([]string{"foo", "bar"}), NewStaticStringArray([]string{"foo", "bar"})},
|
|
{NewStaticBooleanArray([]bool{}), NewStaticBooleanArray(nil)},
|
|
{NewStaticBooleanArray([]bool{true, false}), NewStaticBooleanArray([]bool{true, false})},
|
|
// Status and int comparison
|
|
{NewStaticStatus(StatusError), NewStaticInt(0)},
|
|
{NewStaticStatus(StatusOk), NewStaticInt(1)},
|
|
{NewStaticStatus(StatusUnset), NewStaticInt(2)},
|
|
{NewStaticInt(2), NewStaticStatus(StatusUnset)},
|
|
}
|
|
areNotEqual := []struct {
|
|
lhs, rhs Static
|
|
}{
|
|
{NewStaticInt(1), NewStaticInt(2)},
|
|
{NewStaticBool(true), NewStaticBool(false)},
|
|
{NewStaticBool(true), NewStaticInt(1)},
|
|
{NewStaticInt(1), NewStaticBool(true)},
|
|
{NewStaticString("foo"), NewStaticString("bar")},
|
|
{NewStaticKind(KindClient), NewStaticKind(KindConsumer)},
|
|
{NewStaticStatus(StatusError), NewStaticStatus(StatusOk)},
|
|
{NewStaticStatus(StatusOk), NewStaticStatus(StatusUnset)},
|
|
{NewStaticStatus(StatusOk), NewStaticKind(KindInternal)},
|
|
{NewStaticStatus(StatusError), NewStaticFloat(0)},
|
|
{NewStaticFloat(0), NewStaticStatus(StatusError)},
|
|
{NewStaticIntArray([]int{}), NewStaticIntArray([]int{0})},
|
|
{NewStaticIntArray([]int{111, 11}), NewStaticIntArray([]int{11, 111})},
|
|
{NewStaticFloatArray([]float64{}), NewStaticFloatArray([]float64{0.0})},
|
|
{NewStaticFloatArray([]float64{1.1, 2.2}), NewStaticFloatArray([]float64{2.2, 1.1})},
|
|
{NewStaticStringArray([]string{}), NewStaticStringArray([]string{""})},
|
|
{NewStaticStringArray([]string{"foo", "bar"}), NewStaticStringArray([]string{"bar", "foo"})},
|
|
{NewStaticBooleanArray([]bool{}), NewStaticBooleanArray([]bool{true})},
|
|
{NewStaticBooleanArray([]bool{true, false}), NewStaticBooleanArray([]bool{false, true})},
|
|
}
|
|
allNilComparisonsAreFalse := []struct {
|
|
lhs, rhs Static
|
|
}{
|
|
{NewStaticInt(1), StaticNil},
|
|
{NewStaticString(""), StaticNil},
|
|
{NewStaticStatus(StatusError), StaticNil},
|
|
}
|
|
for _, tt := range areEqual {
|
|
t.Run(fmt.Sprintf("%s==%s", testName(tt.rhs), testName(tt.rhs)), func(t *testing.T) {
|
|
assert.True(t, tt.lhs.Equals(&tt.rhs))
|
|
assert.True(t, tt.rhs.Equals(&tt.lhs))
|
|
})
|
|
}
|
|
for _, tt := range areNotEqual {
|
|
t.Run(fmt.Sprintf("%s!=%s", testName(tt.lhs), testName(tt.rhs)), func(t *testing.T) {
|
|
assert.True(t, tt.lhs.NotEquals(&tt.rhs))
|
|
assert.True(t, tt.rhs.NotEquals(&tt.lhs))
|
|
})
|
|
}
|
|
for _, tt := range allNilComparisonsAreFalse {
|
|
t.Run(fmt.Sprintf("%s==%s", testName(tt.lhs), testName(tt.rhs)), func(t *testing.T) {
|
|
assert.False(t, tt.lhs.Equals(&tt.rhs))
|
|
assert.False(t, tt.rhs.Equals(&tt.lhs))
|
|
assert.False(t, tt.lhs.NotEquals(&tt.rhs))
|
|
assert.False(t, tt.rhs.NotEquals(&tt.lhs))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_compare(t *testing.T) {
|
|
testCases := []struct {
|
|
s1, s2 Static
|
|
want int
|
|
}{
|
|
{s1: NewStaticInt(10), s2: NewStaticInt(5), want: 1},
|
|
{s1: NewStaticInt(5), s2: NewStaticInt(-10), want: 1},
|
|
{s1: NewStaticInt(20), s2: NewStaticInt(20), want: 0},
|
|
{s1: NewStaticFloat(10.5), s2: NewStaticFloat(5.5), want: 1},
|
|
{s1: NewStaticFloat(100.0), s2: NewStaticInt(100), want: 0},
|
|
{s1: NewStaticFloat(100.0), s2: NewStaticInt(50), want: 1},
|
|
{s1: NewStaticString("world"), s2: NewStaticString("hello"), want: 1},
|
|
{s1: NewStaticBool(true), s2: NewStaticBool(false), want: 1},
|
|
{s1: NewStaticDuration(10 * time.Second), s2: NewStaticDuration(5 * time.Second), want: 1},
|
|
{s1: NewStaticDuration(10), s2: NewStaticInt(10), want: 0},
|
|
{s1: NewStaticIntArray([]int{1, 3, 3}), s2: NewStaticIntArray([]int{1, -2, 3}), want: 1},
|
|
{s1: NewStaticIntArray([]int{1, 2, 3}), s2: NewStaticIntArray([]int{1, 2, 3}), want: 0},
|
|
{s1: NewStaticFloatArray([]float64{1.1, math.SmallestNonzeroFloat64}), s2: NewStaticFloatArray([]float64{1.1, 0}), want: 1},
|
|
{s1: NewStaticFloatArray([]float64{1.1, 2.2, 3.3}), s2: NewStaticFloatArray([]float64{1.1, -2.2, 3.3}), want: 1},
|
|
{s1: NewStaticFloatArray([]float64{1.1, 2.2, 3.3}), s2: NewStaticFloatArray([]float64{1.1, 2.2, 3.3}), want: 0},
|
|
{s1: NewStaticStringArray([]string{"a", "b", "c"}), s2: NewStaticStringArray([]string{"a", "b"}), want: 1},
|
|
{s1: NewStaticStringArray([]string{"a", "b"}), s2: NewStaticStringArray([]string{"a", "b"}), want: 0},
|
|
{s1: NewStaticBooleanArray([]bool{true, false, true}), s2: NewStaticBooleanArray([]bool{true, false}), want: 1},
|
|
{s1: NewStaticBooleanArray([]bool{true, false}), s2: NewStaticBooleanArray([]bool{true, false}), want: 0},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(fmt.Sprintf("%s<>%s", tt.s1.String(), tt.s2.String()), func(t *testing.T) {
|
|
res := tt.s1.compare(&tt.s2)
|
|
require.Equal(t, tt.want, res, "s1.compare(s2)")
|
|
res = tt.s2.compare(&tt.s1)
|
|
require.Equal(t, -tt.want, res, "s2.compare(s1)")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_sumInto(t *testing.T) {
|
|
tests := []struct {
|
|
s1, s2, want Static
|
|
}{
|
|
{NewStaticInt(1), NewStaticInt(2), NewStaticInt(3)},
|
|
{NewStaticInt(-3), NewStaticInt(2), NewStaticInt(-1)},
|
|
{NewStaticInt(-3), NewStaticDuration(3), NewStaticInt(-3)},
|
|
{NewStaticDuration(2 * time.Second), NewStaticDuration(1 * time.Second), NewStaticDuration(3 * time.Second)},
|
|
{NewStaticDuration(2 * time.Second), NewStaticInt(3000), NewStaticDuration(2 * time.Second)},
|
|
{NewStaticFloat(1.5), NewStaticFloat(2.5), NewStaticFloat(4.0)},
|
|
{NewStaticFloat(-4.5), NewStaticFloat(2.0), NewStaticFloat(-2.5)},
|
|
{NewStaticFloat(3.14), NewStaticInt(1), NewStaticFloat(3.14)},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(fmt.Sprintf("%s+%s", tt.s1.String(), tt.s2.String()), func(t *testing.T) {
|
|
tt.s1.sumInto(&tt.s2)
|
|
assert.Equal(t, tt.want, tt.s1, "s1.sumInto(s2)")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_divideBy(t *testing.T) {
|
|
tests := []struct {
|
|
s Static
|
|
f float64
|
|
want Static
|
|
}{
|
|
{NewStaticInt(10), 2, NewStaticFloat(5)},
|
|
{NewStaticDuration(12 * time.Second), 2.1, NewStaticDuration(6 * time.Second)},
|
|
{NewStaticFloat(12.2), 2.0, NewStaticFloat(6.1)},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(fmt.Sprintf("%s/%.2f", tt.s.String(), tt.f), func(t *testing.T) {
|
|
s := tt.s.divideBy(tt.f)
|
|
assert.Equal(t, tt.want, s, "s.divideBy(f)")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatic_append(t *testing.T) {
|
|
tests := []struct {
|
|
s1, s2, want Static
|
|
}{
|
|
// int
|
|
{s1: NewStaticInt(1), s2: NewStaticInt(2), want: NewStaticIntArray([]int{1, 2})},
|
|
{s1: NewStaticInt(1), s2: NewStaticIntArray([]int{2, 3}), want: NewStaticIntArray([]int{1, 2, 3})},
|
|
{s1: NewStaticIntArray([]int{1, 2}), s2: NewStaticInt(3), want: NewStaticIntArray([]int{1, 2, 3})},
|
|
{s1: NewStaticIntArray([]int{1, 2}), s2: NewStaticIntArray([]int{3, 4}), want: NewStaticIntArray([]int{1, 2, 3, 4})},
|
|
{s1: NewStaticIntArray([]int{1, 2}), s2: NewStaticDuration(1 * time.Second), want: NewStaticNil()},
|
|
{s1: NewStaticIntArray([]int{1, 2}), s2: NewStaticString("1"), want: NewStaticNil()},
|
|
// float
|
|
{s1: NewStaticFloat(1.1), s2: NewStaticFloat(2.2), want: NewStaticFloatArray([]float64{1.1, 2.2})},
|
|
{s1: NewStaticFloat(1.1), s2: NewStaticFloatArray([]float64{2.2, 3.3}), want: NewStaticFloatArray([]float64{1.1, 2.2, 3.3})},
|
|
{s1: NewStaticFloatArray([]float64{1.1, 2.2}), s2: NewStaticFloat(3.3), want: NewStaticFloatArray([]float64{1.1, 2.2, 3.3})},
|
|
{s1: NewStaticFloatArray([]float64{1.1, 2.2}), s2: NewStaticFloatArray([]float64{3.3, 4.4}), want: NewStaticFloatArray([]float64{1.1, 2.2, 3.3, 4.4})},
|
|
{s1: NewStaticFloatArray([]float64{1.1, 2.2}), s2: NewStaticDuration(1 * time.Second), want: NewStaticNil()},
|
|
{s1: NewStaticFloatArray([]float64{1.1, 2.2}), s2: NewStaticString("1.1"), want: NewStaticNil()},
|
|
// string
|
|
{s1: NewStaticString("1"), s2: NewStaticString("2"), want: NewStaticStringArray([]string{"1", "2"})},
|
|
{s1: NewStaticString("1"), s2: NewStaticStringArray([]string{"2", "3"}), want: NewStaticStringArray([]string{"1", "2", "3"})},
|
|
{s1: NewStaticStringArray([]string{"1", "2"}), s2: NewStaticString("3"), want: NewStaticStringArray([]string{"1", "2", "3"})},
|
|
{s1: NewStaticStringArray([]string{"1", "2"}), s2: NewStaticStringArray([]string{"3", "4"}), want: NewStaticStringArray([]string{"1", "2", "3", "4"})},
|
|
{s1: NewStaticStringArray([]string{"1", "2"}), s2: NewStaticInt(1), want: NewStaticNil()},
|
|
// bool
|
|
{s1: NewStaticBool(true), s2: NewStaticBool(false), want: NewStaticBooleanArray([]bool{true, false})},
|
|
{s1: NewStaticBool(true), s2: NewStaticBooleanArray([]bool{false, true}), want: NewStaticBooleanArray([]bool{true, false, true})},
|
|
{s1: NewStaticBooleanArray([]bool{true, false}), s2: NewStaticBool(true), want: NewStaticBooleanArray([]bool{true, false, true})},
|
|
{s1: NewStaticBooleanArray([]bool{true, false}), s2: NewStaticBooleanArray([]bool{true, false}), want: NewStaticBooleanArray([]bool{true, false, true, false})},
|
|
{s1: NewStaticBooleanArray([]bool{true, false}), s2: NewStaticInt(1), want: NewStaticNil()},
|
|
}
|
|
for _, tt := range tests {
|
|
res, ok := tt.s1.append(tt.s2)
|
|
assert.Equal(t, tt.want, res)
|
|
if res.IsNil() {
|
|
assert.False(t, ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPipelineExtractConditions(t *testing.T) {
|
|
testCases := []struct {
|
|
query string
|
|
request FetchSpansRequest
|
|
}{
|
|
{
|
|
"{ .foo1 = `a` } | { .foo2 = `b` }",
|
|
FetchSpansRequest{
|
|
Conditions: []Condition{
|
|
newCondition(NewAttribute("foo1"), OpEqual, NewStaticString("a")),
|
|
newCondition(NewAttribute("foo2"), OpEqual, NewStaticString("b")),
|
|
},
|
|
AllConditions: true,
|
|
},
|
|
},
|
|
{
|
|
"{ .foo = `a` } | by(.namespace) | count() > 3",
|
|
FetchSpansRequest{
|
|
Conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpEqual, NewStaticString("a")),
|
|
newCondition(NewAttribute("namespace"), OpNone),
|
|
},
|
|
AllConditions: false,
|
|
},
|
|
},
|
|
{
|
|
"{ .foo = `a` } | avg(duration) > 20ms",
|
|
FetchSpansRequest{
|
|
Conditions: []Condition{
|
|
newCondition(NewAttribute("foo"), OpEqual, NewStaticString("a")),
|
|
newCondition(NewIntrinsic(IntrinsicDuration), OpNone),
|
|
},
|
|
AllConditions: false,
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.query, func(t *testing.T) {
|
|
ast, err := Parse(tc.query)
|
|
require.NoError(t, err)
|
|
|
|
actualRequest := FetchSpansRequest{
|
|
AllConditions: true,
|
|
}
|
|
ast.Pipeline.extractConditions(&actualRequest)
|
|
require.Equal(t, tc.request, actualRequest)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPipelineEvaluate(t *testing.T) {
|
|
testCases := []struct {
|
|
query string
|
|
input []*Spanset
|
|
output []*Spanset
|
|
}{
|
|
{
|
|
"{ true } | { true } | { true }",
|
|
[]*Spanset{
|
|
{Spans: []Span{&mockSpan{}}},
|
|
},
|
|
[]*Spanset{
|
|
{Spans: []Span{&mockSpan{}}},
|
|
},
|
|
},
|
|
{
|
|
"{ true } | { false } | { true }",
|
|
[]*Spanset{
|
|
{Spans: []Span{&mockSpan{}}},
|
|
},
|
|
[]*Spanset{},
|
|
},
|
|
{
|
|
"{ .foo1 = `a` } | { .foo2 = `b` }",
|
|
[]*Spanset{
|
|
{Spans: []Span{
|
|
// First span should be dropped here
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo1"): NewStaticString("a")}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo1"): NewStaticString("a"), NewAttribute("foo2"): NewStaticString("b")}},
|
|
}},
|
|
},
|
|
[]*Spanset{
|
|
{Spans: []Span{
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo1"): NewStaticString("a"), NewAttribute("foo2"): NewStaticString("b")}},
|
|
}},
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.query, func(t *testing.T) {
|
|
ast, err := Parse(tc.query)
|
|
require.NoError(t, err)
|
|
|
|
actual, err := ast.Pipeline.evaluate(tc.input)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.output, actual)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSpansetFilterEvaluate(t *testing.T) {
|
|
testCases := []struct {
|
|
query string
|
|
input []*Spanset
|
|
output []*Spanset
|
|
}{
|
|
{
|
|
"{ true }",
|
|
[]*Spanset{
|
|
// Empty spanset is dropped
|
|
{Spans: []Span{}},
|
|
{Spans: []Span{&mockSpan{}}},
|
|
},
|
|
[]*Spanset{
|
|
{Spans: []Span{&mockSpan{}}},
|
|
},
|
|
},
|
|
{
|
|
"{ .foo = `a` }",
|
|
[]*Spanset{
|
|
{Spans: []Span{
|
|
// Second span should be dropped here
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticString("a")}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticString("b")}},
|
|
}},
|
|
{Spans: []Span{
|
|
// This entire spanset will be dropped
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticString("b")}},
|
|
}},
|
|
},
|
|
[]*Spanset{
|
|
{Spans: []Span{
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticString("a")}},
|
|
}},
|
|
},
|
|
},
|
|
{
|
|
"{ .http.status > `200` }",
|
|
[]*Spanset{
|
|
{Spans: []Span{
|
|
// First span should be dropped here
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("200")}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("201")}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("300")}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("301")}},
|
|
}},
|
|
{Spans: []Span{
|
|
// This entire spanset will be dropped
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("100")}},
|
|
}},
|
|
},
|
|
[]*Spanset{
|
|
{Spans: []Span{
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("201")}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("300")}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("301")}},
|
|
}},
|
|
},
|
|
},
|
|
{
|
|
"{ .http.status <= `300` }",
|
|
[]*Spanset{
|
|
{Spans: []Span{
|
|
// Last span should be dropped here
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("200")}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("201")}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("300")}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("301")}},
|
|
}},
|
|
{Spans: []Span{
|
|
// This entire spanset is valid
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("100")}},
|
|
}},
|
|
},
|
|
[]*Spanset{
|
|
{Spans: []Span{
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("200")}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("201")}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("300")}},
|
|
}},
|
|
{Spans: []Span{
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticString("100")}},
|
|
}},
|
|
},
|
|
},
|
|
{
|
|
"{ .http.status > `200` }",
|
|
[]*Spanset{
|
|
{Spans: []Span{
|
|
// This entire spanset will be dropped because mismatch type
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticInt(200)}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticInt(201)}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticInt(300)}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticInt(301)}},
|
|
}},
|
|
{Spans: []Span{
|
|
// This entire spanset will be dropped because mismatch type
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("http.status"): NewStaticInt(100)}},
|
|
}},
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
"{ .foo = 1 || (.foo >= 4 && .foo < 6) }",
|
|
[]*Spanset{
|
|
{Spans: []Span{
|
|
// Second span should be dropped here
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticInt(1)}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticInt(2)}},
|
|
}},
|
|
{Spans: []Span{
|
|
// First span should be dropped here
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticInt(3)}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticInt(4)}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticInt(5)}},
|
|
}},
|
|
{Spans: []Span{
|
|
// Entire spanset should be dropped
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticInt(6)}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticInt(7)}},
|
|
}},
|
|
},
|
|
[]*Spanset{
|
|
{Spans: []Span{
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticInt(1)}},
|
|
}},
|
|
{Spans: []Span{
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticInt(4)}},
|
|
&mockSpan{attributes: map[Attribute]Static{NewAttribute("foo"): NewStaticInt(5)}},
|
|
}},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.query, func(t *testing.T) {
|
|
ast, err := Parse(tc.query)
|
|
require.NoError(t, err)
|
|
|
|
filt := ast.Pipeline.Elements[0].(*SpansetFilter)
|
|
|
|
actual, err := filt.evaluate(tc.input)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.output, actual)
|
|
})
|
|
}
|
|
}
|
|
|
|
var _ Span = (*mockSpan)(nil)
|
|
|
|
type mockSpan struct {
|
|
id []byte
|
|
startTimeUnixNanos uint64
|
|
durationNanos uint64
|
|
attributes map[Attribute]Static
|
|
|
|
parentID, left, right int
|
|
}
|
|
|
|
func newMockSpan(id []byte) *mockSpan {
|
|
return &mockSpan{
|
|
id: id,
|
|
attributes: map[Attribute]Static{},
|
|
}
|
|
}
|
|
|
|
func (m *mockSpan) WithStartTime(nanos uint64) *mockSpan {
|
|
m.startTimeUnixNanos = nanos
|
|
return m
|
|
}
|
|
|
|
func (m *mockSpan) WithDuration(nanos uint64) *mockSpan {
|
|
m.durationNanos = nanos
|
|
return m
|
|
}
|
|
|
|
func (m *mockSpan) WithNestedSetInfo(parentid, left, right int) *mockSpan {
|
|
m.parentID = parentid
|
|
m.left = left
|
|
m.right = right
|
|
return m
|
|
}
|
|
|
|
func (m *mockSpan) WithSpanString(key string, value string) *mockSpan {
|
|
m.attributes[NewScopedAttribute(AttributeScopeSpan, false, key)] = NewStaticString(value)
|
|
return m
|
|
}
|
|
|
|
func (m *mockSpan) WithSpanInt(key string, value int) *mockSpan {
|
|
m.attributes[NewScopedAttribute(AttributeScopeSpan, false, key)] = NewStaticInt(value)
|
|
return m
|
|
}
|
|
|
|
func (m *mockSpan) WithSpanFloat(key string, value float64) *mockSpan {
|
|
m.attributes[NewScopedAttribute(AttributeScopeSpan, false, key)] = NewStaticFloat(value)
|
|
return m
|
|
}
|
|
|
|
func (m *mockSpan) WithAttrBool(key string, value bool) *mockSpan {
|
|
m.attributes[NewAttribute(key)] = NewStaticBool(value)
|
|
return m
|
|
}
|
|
|
|
func (m *mockSpan) AttributeFor(a Attribute) (Static, bool) {
|
|
s, ok := m.attributes[a]
|
|
// if not found explicitly, check if it's a span attribute
|
|
if !ok && a.Scope == AttributeScopeNone {
|
|
aSpan := a
|
|
aSpan.Scope = AttributeScopeSpan
|
|
s, ok = m.attributes[aSpan]
|
|
}
|
|
// if not found explicitly, check if it's a resource attribute
|
|
if !ok && a.Scope == AttributeScopeNone {
|
|
aRes := a
|
|
aRes.Scope = AttributeScopeResource
|
|
s, ok = m.attributes[aRes]
|
|
}
|
|
return s, ok
|
|
}
|
|
|
|
func (m *mockSpan) AllAttributes() map[Attribute]Static {
|
|
return m.attributes
|
|
}
|
|
|
|
func (m *mockSpan) AllAttributesFunc(cb func(Attribute, Static)) {
|
|
for k, v := range m.attributes {
|
|
cb(k, v)
|
|
}
|
|
}
|
|
|
|
func (m *mockSpan) ID() []byte {
|
|
return m.id
|
|
}
|
|
|
|
func (m *mockSpan) StartTimeUnixNanos() uint64 {
|
|
return m.startTimeUnixNanos
|
|
}
|
|
|
|
func (m *mockSpan) DurationNanos() uint64 {
|
|
return m.durationNanos
|
|
}
|
|
|
|
func (m *mockSpan) DescendantOf(lhs []Span, rhs []Span, falseForAll bool, invert bool, _ bool, _ []Span) []Span {
|
|
return loop(lhs, rhs, falseForAll, invert, descendantOf)
|
|
}
|
|
|
|
func descendantOf(s1 Span, s2 Span) bool {
|
|
return s2.(*mockSpan).left > s1.(*mockSpan).left && s2.(*mockSpan).left < s1.(*mockSpan).right
|
|
}
|
|
|
|
func (m *mockSpan) SiblingOf(lhs []Span, rhs []Span, falseForAll bool, _ bool, _ []Span) []Span {
|
|
return loop(lhs, rhs, falseForAll, false, siblingOf)
|
|
}
|
|
|
|
func siblingOf(s1 Span, s2 Span) bool {
|
|
return s1.(*mockSpan).parentID == s2.(*mockSpan).parentID
|
|
}
|
|
|
|
func (m *mockSpan) ChildOf(lhs []Span, rhs []Span, falseForAll bool, invert bool, _ bool, _ []Span) []Span {
|
|
return loop(lhs, rhs, falseForAll, invert, childOf)
|
|
}
|
|
|
|
func childOf(s1 Span, s2 Span) bool {
|
|
return s1.(*mockSpan).left == s2.(*mockSpan).parentID
|
|
}
|
|
|
|
func loop(lhs []Span, rhs []Span, falseForAll bool, invert bool, eval func(s1 Span, s2 Span) bool) []Span {
|
|
out := []Span{}
|
|
|
|
for _, r := range rhs {
|
|
match := false
|
|
for _, l := range lhs {
|
|
if invert {
|
|
match = eval(r, l)
|
|
} else {
|
|
match = eval(l, r)
|
|
}
|
|
|
|
if match {
|
|
break
|
|
}
|
|
}
|
|
|
|
if (match && !falseForAll) ||
|
|
(!match && falseForAll) {
|
|
out = append(out, r)
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func newStatic(val any) Static {
|
|
if val == nil {
|
|
return NewStaticNil()
|
|
}
|
|
|
|
switch v := val.(type) {
|
|
case int:
|
|
return NewStaticInt(v)
|
|
case float64:
|
|
return NewStaticFloat(v)
|
|
case string:
|
|
return NewStaticString(v)
|
|
case bool:
|
|
return NewStaticBool(v)
|
|
case time.Duration:
|
|
return NewStaticDuration(v)
|
|
case Status:
|
|
return NewStaticStatus(v)
|
|
case Kind:
|
|
return NewStaticKind(v)
|
|
case []int:
|
|
return NewStaticIntArray(v)
|
|
case []float64:
|
|
return NewStaticFloatArray(v)
|
|
case []string:
|
|
return NewStaticStringArray(v)
|
|
case []bool:
|
|
return NewStaticBooleanArray(v)
|
|
default:
|
|
panic(fmt.Sprintf("unsupported type %T", val))
|
|
}
|
|
}
|
|
|
|
func testName(val any) string {
|
|
if val == nil {
|
|
return "nil"
|
|
}
|
|
|
|
switch v := val.(type) {
|
|
case float64:
|
|
return fmt.Sprintf("%e", v)
|
|
case []int:
|
|
return fmt.Sprintf("[%d]int", len(v))
|
|
case []float64:
|
|
return fmt.Sprintf("[%d]float", len(v))
|
|
case []string:
|
|
return fmt.Sprintf("[%d]str", len(v))
|
|
case []bool:
|
|
return fmt.Sprintf("[%d]bool", len(v))
|
|
case Static:
|
|
return v.EncodeToString(false)
|
|
default:
|
|
return fmt.Sprintf("%v", v)
|
|
}
|
|
}
|
|
|
|
func TestNeedsFullTrace(t *testing.T) {
|
|
dontNeedFullTrace := []string{
|
|
"{}",
|
|
"{} | rate()",
|
|
}
|
|
|
|
needFullTrace := []string{
|
|
"{} >> {}",
|
|
"{} && {}",
|
|
"{} | count() > 100",
|
|
"({} | count()) = ({} | count())",
|
|
}
|
|
|
|
for _, test := range dontNeedFullTrace {
|
|
t.Run(test, func(t *testing.T) {
|
|
expr, err := Parse(test)
|
|
require.NoError(t, err)
|
|
require.Equal(t, false, expr.NeedsFullTrace())
|
|
})
|
|
}
|
|
|
|
for _, test := range needFullTrace {
|
|
t.Run(test, func(t *testing.T) {
|
|
expr, err := Parse(test)
|
|
require.NoError(t, err)
|
|
require.Equal(t, true, expr.NeedsFullTrace())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewBinaryOperation_TraceIDNormalization(t *testing.T) {
|
|
traceIDAttr := NewIntrinsic(IntrinsicTraceID)
|
|
op := OpEqual
|
|
|
|
tests := []struct {
|
|
name string
|
|
expression FieldExpression
|
|
expected FieldExpression
|
|
}{
|
|
{
|
|
name: "with leading zeros",
|
|
expression: NewStaticString("0000123"),
|
|
expected: NewStaticString("123"),
|
|
},
|
|
{
|
|
name: "no leading zeros",
|
|
expression: NewStaticString("123456789abcdef123456789abcdef"),
|
|
expected: NewStaticString("123456789abcdef123456789abcdef"),
|
|
},
|
|
{
|
|
name: "int",
|
|
expression: NewStaticInt(123),
|
|
expected: NewStaticInt(123),
|
|
},
|
|
{
|
|
name: "float",
|
|
expression: NewStaticFloat(123.0),
|
|
expected: NewStaticFloat(123.0),
|
|
},
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
reverse bool
|
|
}{
|
|
{name: "trace:id = value", reverse: false},
|
|
{name: "value = trace:id", reverse: true},
|
|
} {
|
|
for _, tt := range tests {
|
|
t.Run(tc.name+" "+tt.name, func(t *testing.T) {
|
|
var lhs FieldExpression = traceIDAttr
|
|
rhs := tt.expression
|
|
var expectedLHS FieldExpression = traceIDAttr
|
|
expectedRHS := tt.expected
|
|
|
|
if tc.reverse {
|
|
lhs = tt.expression
|
|
rhs = traceIDAttr
|
|
expectedLHS = tt.expected
|
|
expectedRHS = traceIDAttr
|
|
}
|
|
|
|
binop := newBinaryOperation(op, lhs, rhs)
|
|
|
|
// Verify the result is a BinaryOperation
|
|
require.IsType(t, &BinaryOperation{}, binop)
|
|
actualBinop := binop.(*BinaryOperation)
|
|
assert.Equal(t, expectedLHS, actualBinop.LHS)
|
|
assert.Equal(t, expectedRHS, actualBinop.RHS)
|
|
})
|
|
}
|
|
}
|
|
}
|