Files
Joe ElliottandGitHub ef09c5920b [BUGFIX] Fix tag handling when non-attribute runes are present (#6223)
* fix

Signed-off-by: Joe Elliott <[email protected]>

* test cases

Signed-off-by: Joe Elliott <[email protected]>

* fix integration test

Signed-off-by: Joe Elliott <[email protected]>

* more test cases

Signed-off-by: Joe Elliott <[email protected]>

* changelog

Signed-off-by: Joe Elliott <[email protected]>

* i will have your head, linter

Signed-off-by: Joe Elliott <[email protected]>

* fix unit tests

Signed-off-by: Joe Elliott <[email protected]>

* fixed integration tests

Signed-off-by: Joe Elliott <[email protected]>

* we call this "marty's test"

Signed-off-by: Joe Elliott <[email protected]>

---------

Signed-off-by: Joe Elliott <[email protected]>
2026-01-15 15:23:47 -05:00

283 lines
10 KiB
Go

package traceql
import (
"strings"
"testing"
"text/scanner"
"time"
"github.com/stretchr/testify/require"
)
type lexerTestCase struct {
input string
expected []int
}
func TestLexerAttributes(t *testing.T) {
testLexer(t, ([]lexerTestCase{
// attributes
{`.foo`, []int{DOT, IDENTIFIER, END_ATTRIBUTE}},
{`."foo".baz."bar"`, []int{DOT, IDENTIFIER, END_ATTRIBUTE}},
{`.foo."bar \" baz"."bar"`, []int{DOT, IDENTIFIER, END_ATTRIBUTE}},
{`.foo."baz \\".bar`, []int{DOT, IDENTIFIER, END_ATTRIBUTE}},
{`."foo.bar"`, []int{DOT, IDENTIFIER, END_ATTRIBUTE}},
{`.count`, []int{DOT, IDENTIFIER, END_ATTRIBUTE}},
{`.foo3`, []int{DOT, IDENTIFIER, END_ATTRIBUTE}},
{`.foo+bar`, []int{DOT, IDENTIFIER, END_ATTRIBUTE}},
{`.foo-bar`, []int{DOT, IDENTIFIER, END_ATTRIBUTE}},
// parent attributes
{`parent.foo`, []int{PARENT_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`parent.count`, []int{PARENT_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`parent.foo3`, []int{PARENT_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`parent.foo+bar`, []int{PARENT_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`parent.foo-bar`, []int{PARENT_DOT, IDENTIFIER, END_ATTRIBUTE}},
// span attributes
{`span.foo`, []int{SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`span.count`, []int{SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`span.foo3`, []int{SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`span.foo+bar`, []int{SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`span.foo-bar`, []int{SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
// resource attributes
{`resource.foo`, []int{RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`resource.count`, []int{RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`resource.foo3`, []int{RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`resource.foo+bar`, []int{RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`resource.foo-bar`, []int{RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
// event attributes
{`event.foo`, []int{EVENT_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`event.count`, []int{EVENT_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`event.count`, []int{EVENT_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`event.foo3`, []int{EVENT_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`event.foo+bar`, []int{EVENT_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`event.foo-bar`, []int{EVENT_DOT, IDENTIFIER, END_ATTRIBUTE}},
// link attributes
{`link.foo`, []int{LINK_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`link.count`, []int{LINK_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`link.count`, []int{LINK_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`link.foo3`, []int{LINK_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`link.foo+bar`, []int{LINK_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`link.foo-bar`, []int{LINK_DOT, IDENTIFIER, END_ATTRIBUTE}},
// instrumentation attributes
{`instrumentation.foo`, []int{INSTRUMENTATION_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`instrumentation.count`, []int{INSTRUMENTATION_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`instrumentation.foo3`, []int{INSTRUMENTATION_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`instrumentation.foo+bar`, []int{INSTRUMENTATION_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`instrumentation.foo-bar`, []int{INSTRUMENTATION_DOT, IDENTIFIER, END_ATTRIBUTE}},
// parent span attributes
{`parent.span.foo`, []int{PARENT_DOT, SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`parent.span.count`, []int{PARENT_DOT, SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`parent.span.foo3`, []int{PARENT_DOT, SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`parent.span.foo+bar`, []int{PARENT_DOT, SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`parent.span.foo-bar`, []int{PARENT_DOT, SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
// parent resource attributes
{`parent.resource.foo`, []int{PARENT_DOT, RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`parent.resource.count`, []int{PARENT_DOT, RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`parent.resource.foo3`, []int{PARENT_DOT, RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`parent.resource.foo+bar`, []int{PARENT_DOT, RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`parent.resource.foo-bar`, []int{PARENT_DOT, RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
// attribute enders: <space>, {, }, (, ), <comma> all force end an attribute
{`.foo .bar`, []int{DOT, IDENTIFIER, END_ATTRIBUTE, DOT, IDENTIFIER, END_ATTRIBUTE}},
{`.foo}.bar`, []int{DOT, IDENTIFIER, END_ATTRIBUTE, CLOSE_BRACE, DOT, IDENTIFIER, END_ATTRIBUTE}},
{`.foo{.bar`, []int{DOT, IDENTIFIER, END_ATTRIBUTE, OPEN_BRACE, DOT, IDENTIFIER, END_ATTRIBUTE}},
{`.foo).bar`, []int{DOT, IDENTIFIER, END_ATTRIBUTE, CLOSE_PARENS, DOT, IDENTIFIER, END_ATTRIBUTE}},
{`.foo(.bar`, []int{DOT, IDENTIFIER, END_ATTRIBUTE, OPEN_PARENS, DOT, IDENTIFIER, END_ATTRIBUTE}},
{`.foo,.bar`, []int{DOT, IDENTIFIER, END_ATTRIBUTE, COMMA, DOT, IDENTIFIER, END_ATTRIBUTE}},
{`. foo`, []int{DOT, END_ATTRIBUTE, IDENTIFIER}},
// not attributes
{`.3`, []int{FLOAT}},
{`.24h`, []int{DURATION}},
}))
}
func TestLexerScopedIntrinsic(t *testing.T) {
testLexer(t, ([]lexerTestCase{
// trace scoped intrinsics
{`trace:duration`, []int{TRACE_COLON, IDURATION}},
{`trace:rootName`, []int{TRACE_COLON, ROOTNAME}},
{`trace:rootService`, []int{TRACE_COLON, ROOTSERVICE}},
{`trace:id`, []int{TRACE_COLON, ID}},
// span scoped intrinsics
{`span:duration`, []int{SPAN_COLON, IDURATION}},
{`span:name`, []int{SPAN_COLON, NAME}},
{`span:kind`, []int{SPAN_COLON, KIND}},
{`span:status`, []int{SPAN_COLON, STATUS}},
{`span:statusMessage`, []int{SPAN_COLON, STATUS_MESSAGE}},
{`span:id`, []int{SPAN_COLON, ID}},
{`span:parentID`, []int{SPAN_COLON, PARENT_ID}},
{`span:childCount`, []int{SPAN_COLON, CHILDCOUNT}},
// event scoped intrinsics
{`event:name`, []int{EVENT_COLON, NAME}},
{`event:timeSinceStart`, []int{EVENT_COLON, TIMESINCESTART}},
// link scoped intrinsics
{`link:traceID`, []int{LINK_COLON, TRACE_ID}},
{`link:spanID`, []int{LINK_COLON, SPAN_ID}},
// instrumentation scoped intrinsics
{`instrumentation:name`, []int{INSTRUMENTATION_COLON, NAME}},
{`instrumentation:version`, []int{INSTRUMENTATION_COLON, VERSION}},
}))
}
func TestLexerIntrinsics(t *testing.T) {
testLexer(t, ([]lexerTestCase{
{`nestedSetLeft`, []int{NESTEDSETLEFT}},
{`nestedSetRight`, []int{NESTEDSETRIGHT}},
{`duration`, []int{IDURATION}},
}))
}
func TestLexerMultitokens(t *testing.T) {
testLexer(t, ([]lexerTestCase{
// attributes
{`&&`, []int{AND}},
{`>>`, []int{DESC}},
{`!<<`, []int{NOT_ANCE}},
{`!`, []int{NOT}},
{`!~`, []int{NRE}},
{`&>>`, []int{UNION_DESC}},
}))
}
func TestLexerDuration(t *testing.T) {
testLexer(t, ([]lexerTestCase{
// duration
{"1ns", []int{DURATION}},
{"1s", []int{DURATION}},
{"1us", []int{DURATION}},
{"1m", []int{DURATION}},
{"1h", []int{DURATION}},
{"1µs", []int{DURATION}},
{"1y", []int{DURATION}},
{"1w", []int{DURATION}},
{"1d", []int{DURATION}},
{"1h15m30.918273645s", []int{DURATION}},
// not duration
{"1t", []int{INTEGER, IDENTIFIER}},
{"1", []int{INTEGER}},
}))
}
func TestLexerParseDuration(t *testing.T) {
const MICROSECOND = 1000 * time.Nanosecond
const DAY = 24 * time.Hour
const WEEK = 7 * DAY
const YEAR = 365 * DAY
for _, tc := range []struct {
input string
expected time.Duration
}{
{"1ns", time.Nanosecond},
{"1s", time.Second},
{"1us", MICROSECOND},
{"1m", time.Minute},
{"1h", time.Hour},
{"1µs", MICROSECOND},
{"1y", YEAR},
{"1w", WEEK},
{"1d", DAY},
{"1h15m30.918273645s", time.Hour + 15*time.Minute + 30*time.Second + 918273645*time.Nanosecond},
} {
actual, err := parseDuration(tc.input)
require.Equal(t, err, nil)
require.Equal(t, tc.expected, actual)
}
}
func TestLexerScoping(t *testing.T) {
testLexer(t, ([]lexerTestCase{
{`span.foo3`, []int{SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`span.foo+bar`, []int{SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`span.foo-bar`, []int{SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`span.resource.foo`, []int{SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`parent.span.foo`, []int{PARENT_DOT, SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`span.foo.bar`, []int{SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
// resource attributes
{`resource.foo`, []int{RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`resource.count`, []int{RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`resource.foo3`, []int{RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`resource.foo+bar`, []int{RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`resource.foo-bar`, []int{RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`resource.span.foo`, []int{RESOURCE_DOT, IDENTIFIER, END_ATTRIBUTE}},
// parent span attributes
{`parent.span.foo`, []int{PARENT_DOT, SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`parent.span.count`, []int{PARENT_DOT, SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
{`parent.span.resource.id`, []int{PARENT_DOT, SPAN_DOT, IDENTIFIER, END_ATTRIBUTE}},
}))
}
func testLexer(t *testing.T, tcs []lexerTestCase) {
for _, tc := range tcs {
t.Run(tc.input, func(t *testing.T) {
actual := []int{}
l := lexer{
Scanner: scanner.Scanner{
Mode: scanner.SkipComments | scanner.ScanStrings,
},
}
l.Init(strings.NewReader(tc.input))
var lval yySymType
for {
tok := l.Lex(&lval)
if tok == 0 {
break
}
actual = append(actual, tok)
}
require.Equal(t, tc.expected, actual)
})
}
}
func TestContainsNonAttributeRune(t *testing.T) {
tests := []struct {
input string
expected bool
}{
// Valid attribute names (no non-attribute runes)
{"foo", false},
{"foo-bar", false},
{"foo+bar", false},
{"foo3", false},
{"count", false},
{"foo.bar.baz", false},
{"service_name", false},
// Invalid attribute names (contains non-attribute runes)
{"foo bar", true}, // space
{"foo{bar", true}, // {
{"foo}bar", true}, // }
{"foo(bar", true}, // (
{"foo)bar", true}, // )
{"foo=bar", true}, // =
{"foo~bar", true}, // ~
{"foo!bar", true}, // !
{"foo<bar", true}, // <
{"foo>bar", true}, // >
{"foo&bar", true}, // &
{"foo|bar", true}, // |
{"foo^bar", true}, // ^
{"foo,bar", true}, // ,
{" foo", true}, // leading space
{"foo ", true}, // trailing space
// Edge cases
{"", false}, // empty string
}
for _, tc := range tests {
t.Run(tc.input, func(t *testing.T) {
actual := ContainsNonAttributeRune(tc.input)
require.Equal(t, tc.expected, actual)
})
}
}
func BenchmarkIsAttributeRune(b *testing.B) {
for i := 0; i < b.N; i++ {
isAttributeRune('=')
}
}