signoz/pkg/variables/variable_replace_visitor_test.go

475 lines
12 KiB
Go
Raw Normal View History

package variables
import (
"testing"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/stretchr/testify/assert"
)
func TestReplaceVariablesInExpression(t *testing.T) {
tests := []struct {
name string
expression string
variables map[string]qbtypes.VariableItem
expected string
wantErr bool
}{
{
name: "simple string variable replacement",
expression: "service.name = $service",
variables: map[string]qbtypes.VariableItem{
"service": {
Type: qbtypes.DynamicVariableType,
Value: "auth-service",
},
},
expected: "service.name = 'auth-service'",
},
{
name: "simple string variable replacement",
expression: "service.name = $service",
variables: map[string]qbtypes.VariableItem{
"service": {
Type: qbtypes.QueryVariableType,
Value: "auth-service",
},
},
expected: "service.name = 'auth-service'",
},
{
name: "simple string variable replacement",
expression: "service.name = $service",
variables: map[string]qbtypes.VariableItem{
"service": {
Type: qbtypes.CustomVariableType,
Value: "auth-service",
},
},
expected: "service.name = 'auth-service'",
},
{
name: "simple string variable replacement",
expression: "service.name = $service",
variables: map[string]qbtypes.VariableItem{
"service": {
Type: qbtypes.TextBoxVariableType,
Value: "auth-service",
},
},
expected: "service.name = 'auth-service'",
},
{
name: "variable with dollar sign prefix in map",
expression: "service.name = $service",
variables: map[string]qbtypes.VariableItem{
"$service": {
Type: qbtypes.DynamicVariableType,
Value: "auth-service",
},
},
expected: "service.name = 'auth-service'",
},
{
name: "numeric variable replacement",
expression: "http.status_code > $threshold",
variables: map[string]qbtypes.VariableItem{
"threshold": {
Type: qbtypes.DynamicVariableType,
Value: 400,
},
},
expected: "http.status_code > 400",
},
{
name: "boolean variable replacement",
expression: "is_error = $error_flag",
variables: map[string]qbtypes.VariableItem{
"error_flag": {
Type: qbtypes.DynamicVariableType,
Value: true,
},
},
expected: "is_error = true",
},
{
name: "array variable in IN clause",
expression: "service.name IN $services",
variables: map[string]qbtypes.VariableItem{
"services": {
Type: qbtypes.DynamicVariableType,
Value: []any{"auth", "api", "web"},
},
},
expected: "service.name IN ('auth', 'api', 'web')",
},
{
name: "array variable with mixed types",
expression: "id IN $ids",
variables: map[string]qbtypes.VariableItem{
"ids": {
Type: qbtypes.DynamicVariableType,
Value: []any{1, 2, "three", 4.5},
},
},
expected: "id IN (1, 2, 'three', 4.5)",
},
{
name: "multiple variables in expression",
expression: "service.name = $service AND env = $environment AND status_code >= $min_code",
variables: map[string]qbtypes.VariableItem{
"service": {
Type: qbtypes.DynamicVariableType,
Value: "auth-service",
},
"environment": {
Type: qbtypes.DynamicVariableType,
Value: "production",
},
"min_code": {
Type: qbtypes.DynamicVariableType,
Value: 400,
},
},
expected: "service.name = 'auth-service' AND env = 'production' AND status_code >= 400",
},
{
name: "variable in complex expression with parentheses",
expression: "(service.name = $service OR service.name = 'default') AND status_code > $threshold",
variables: map[string]qbtypes.VariableItem{
"service": {
Type: qbtypes.DynamicVariableType,
Value: "auth",
},
"threshold": {
Type: qbtypes.DynamicVariableType,
Value: 200,
},
},
expected: "(service.name = 'auth' OR service.name = 'default') AND status_code > 200",
},
{
name: "variable not found - preserved as is",
expression: "service.name = $unknown_service",
variables: map[string]qbtypes.VariableItem{},
expected: "service.name = $unknown_service",
},
{
name: "string with quotes needs escaping",
expression: "message = $msg",
variables: map[string]qbtypes.VariableItem{
"msg": {
Type: qbtypes.DynamicVariableType,
Value: "user's request",
},
},
expected: "message = 'user\\'s request'",
},
{
name: "dynamic variable with __all__ value",
expression: "service.name = $all_services",
variables: map[string]qbtypes.VariableItem{
"all_services": {
Type: qbtypes.DynamicVariableType,
Value: "__all__",
},
},
expected: "", // Special value preserved
},
{
name: "variable in NOT IN clause",
expression: "service.name NOT IN $excluded",
variables: map[string]qbtypes.VariableItem{
"excluded": {
Type: qbtypes.DynamicVariableType,
Value: []any{"test", "debug"},
},
},
expected: "service.name NOT IN ('test', 'debug')",
},
{
name: "variable in BETWEEN clause",
expression: "latency BETWEEN $min AND $max",
variables: map[string]qbtypes.VariableItem{
"min": {
Type: qbtypes.DynamicVariableType,
Value: 100,
},
"max": {
Type: qbtypes.DynamicVariableType,
Value: 500,
},
},
expected: "latency BETWEEN 100 AND 500",
},
{
name: "variable in LIKE expression",
expression: "service.name LIKE $pattern",
variables: map[string]qbtypes.VariableItem{
"pattern": {
Type: qbtypes.DynamicVariableType,
Value: "%auth%",
},
},
expected: "service.name LIKE '%auth%'",
},
{
name: "variable in function call",
expression: "has(tags, $tag)",
variables: map[string]qbtypes.VariableItem{
"tag": {
Type: qbtypes.DynamicVariableType,
Value: "error",
},
},
expected: "has(tags, 'error')",
},
{
name: "variable in hasAny function",
expression: "hasAny(tags, $tags)",
variables: map[string]qbtypes.VariableItem{
"tags": {
Type: qbtypes.DynamicVariableType,
Value: []any{"error", "warning", "info"},
},
},
expected: "hasAny(tags, ('error', 'warning', 'info'))",
},
{
name: "variable in hasToken function",
expression: "hasToken(tags, $tags)",
variables: map[string]qbtypes.VariableItem{
"tags": {
Type: qbtypes.DynamicVariableType,
Value: "test",
},
},
expected: "hasToken(tags, 'test')",
},
{
name: "empty array variable",
expression: "service.name IN $services",
variables: map[string]qbtypes.VariableItem{
"services": {
Type: qbtypes.DynamicVariableType,
Value: []any{},
},
},
expected: "service.name IN ()",
},
{
name: "expression with OR and variables",
expression: "env = $env1 OR env = $env2",
variables: map[string]qbtypes.VariableItem{
"env1": {
Type: qbtypes.DynamicVariableType,
Value: "staging",
},
"env2": {
Type: qbtypes.DynamicVariableType,
Value: "production",
},
},
expected: "env = 'staging' OR env = 'production'",
},
{
name: "NOT expression with variable",
expression: "NOT service.name = $service",
variables: map[string]qbtypes.VariableItem{
"service": {
Type: qbtypes.DynamicVariableType,
Value: "test-service",
},
},
expected: "NOT service.name = 'test-service'",
},
{
name: "variable in EXISTS clause",
expression: "tags EXISTS",
variables: map[string]qbtypes.VariableItem{},
expected: "tags EXISTS",
},
{
name: "complex nested expression",
expression: "(service.name IN $services AND env = $env) OR (status_code >= $error_code)",
variables: map[string]qbtypes.VariableItem{
"services": {
Type: qbtypes.DynamicVariableType,
Value: []any{"auth", "api"},
},
"env": {
Type: qbtypes.DynamicVariableType,
Value: "prod",
},
"error_code": {
Type: qbtypes.DynamicVariableType,
Value: 500,
},
},
expected: "(service.name IN ('auth', 'api') AND env = 'prod') OR (status_code >= 500)",
},
{
name: "float variable",
expression: "cpu_usage > $threshold",
variables: map[string]qbtypes.VariableItem{
"threshold": {
Type: qbtypes.DynamicVariableType,
Value: 85.5,
},
},
expected: "cpu_usage > 85.5",
},
{
name: "variable in REGEXP expression",
expression: "message REGEXP $pattern",
variables: map[string]qbtypes.VariableItem{
"pattern": {
Type: qbtypes.DynamicVariableType,
Value: "^ERROR.*",
},
},
expected: "message REGEXP '^ERROR.*'",
},
{
name: "variable in NOT REGEXP expression",
expression: "message NOT REGEXP $pattern",
variables: map[string]qbtypes.VariableItem{
"pattern": {
Type: qbtypes.DynamicVariableType,
Value: "^DEBUG.*",
},
},
expected: "message NOT REGEXP '^DEBUG.*'",
},
{
name: "invalid syntax",
expression: "service.name = = $service",
variables: map[string]qbtypes.VariableItem{},
wantErr: true,
},
{
name: "full text search not affected by variables",
expression: "'error message'",
variables: map[string]qbtypes.VariableItem{},
expected: "'error message'",
},
{
name: "comparison operators",
expression: "a < $v1 AND b <= $v2 AND c > $v3 AND d >= $v4 AND e != $v5 AND f <> $v6",
variables: map[string]qbtypes.VariableItem{
"v1": {Type: qbtypes.DynamicVariableType, Value: 10},
"v2": {Type: qbtypes.DynamicVariableType, Value: 20},
"v3": {Type: qbtypes.DynamicVariableType, Value: 30},
"v4": {Type: qbtypes.DynamicVariableType, Value: 40},
"v5": {Type: qbtypes.DynamicVariableType, Value: "test"},
"v6": {Type: qbtypes.DynamicVariableType, Value: "other"},
},
expected: "a < 10 AND b <= 20 AND c > 30 AND d >= 40 AND e != 'test' AND f <> 'other'",
},
{
name: "CONTAINS operator with variable",
expression: "message CONTAINS $text",
variables: map[string]qbtypes.VariableItem{
"text": {
Type: qbtypes.DynamicVariableType,
Value: "error",
},
},
expected: "message CONTAINS 'error'",
},
{
name: "NOT CONTAINS operator with variable",
expression: "message NOT CONTAINS $text",
variables: map[string]qbtypes.VariableItem{
"text": {
Type: qbtypes.DynamicVariableType,
Value: "debug",
},
},
expected: "message NOT CONTAINS 'debug'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ReplaceVariablesInExpression(tt.expression, tt.variables)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
})
}
}
func TestFormatVariableValue(t *testing.T) {
visitor := &variableReplacementVisitor{}
tests := []struct {
name string
value any
expected string
}{
{
name: "string value",
value: "hello",
expected: "'hello'",
},
{
name: "string with single quote",
value: "user's data",
expected: "'user\\'s data'",
},
{
name: "integer value",
value: 42,
expected: "42",
},
{
name: "float value",
value: 3.14,
expected: "3.14",
},
{
name: "boolean true",
value: true,
expected: "true",
},
{
name: "boolean false",
value: false,
expected: "false",
},
{
name: "array of strings",
value: []any{"a", "b", "c"},
expected: "('a', 'b', 'c')",
},
{
name: "array of mixed types",
value: []any{"string", 123, true, 45.6},
expected: "('string', 123, true, 45.6)",
},
{
name: "empty array",
value: []any{},
expected: "()",
},
{
name: "nil value",
value: nil,
expected: "<nil>",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := visitor.formatVariableValue(tt.value)
assert.Equal(t, tt.expected, result)
})
}
}