categraf/inputs/mtail/internal/runtime/compiler/checker/checker_test.go

643 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2016 Google Inc. All Rights Reserved.
// This file is available under the Apache license.
package checker_test
import (
"flag"
"strings"
"testing"
"flashcat.cloud/categraf/inputs/mtail/internal/runtime/compiler/ast"
"flashcat.cloud/categraf/inputs/mtail/internal/runtime/compiler/checker"
"flashcat.cloud/categraf/inputs/mtail/internal/runtime/compiler/parser"
"flashcat.cloud/categraf/inputs/mtail/internal/runtime/compiler/symbol"
"flashcat.cloud/categraf/inputs/mtail/internal/runtime/compiler/types"
"flashcat.cloud/categraf/inputs/mtail/internal/testutil"
"github.com/google/go-cmp/cmp/cmpopts"
)
var checkerTestDebug = flag.Bool("checker_test_debug", false, "Turn on to log AST in tests")
var checkerInvalidPrograms = []struct {
name string
program string
errors []string
}{
{
"undefined named capture group",
"/blurgh/ { $undef++\n }\n",
[]string{"undefined named capture group:1:12-17: Capture group `$undef' was not defined by a regular expression visible to this scope.", "\tTry using `(?P<undef>...)' to name the capture group."},
},
{
"out of bounds capref",
"/(blyurg)/ { $2++ \n}\n",
[]string{"out of bounds capref:1:14-15: Capture group `$2' was not defined by a regular expression " +
"visible to this scope.", "\tCheck that there are at least 2 pairs of parentheses."},
},
{
"undefined decorator",
"@foo {}\n",
[]string{"undefined decorator:1:1-4: Decorator `@foo' is not defined.", "\tTry adding a definition `def foo {}' earlier in the program."},
},
{
"undefined identifier",
"// { x++ \n}\n",
[]string{"undefined identifier:1:6: Identifier `x' not declared.", "\tTry adding `counter x' to the top of the program."},
},
{
"invalid regex 1",
"/foo(/ {}\n",
[]string{"invalid regex 1:1:1-6: error parsing regexp: missing closing ): `foo(`"},
},
{
"invalid regex 2",
"/blurg(?P<x.)/ {}\n",
[]string{"invalid regex 2:1:1-14: error parsing regexp: invalid named capture: `(?P<x.)`"},
},
{
"invalid regex 3",
"/blurg(?P<x>[[:alph:]])/ {}\n",
[]string{"invalid regex 3:1:1-24: error parsing regexp: invalid character class range: `[:alph:]`"},
},
{
"duplicate declaration",
"counter foo\ncounter foo\n",
[]string{
"duplicate declaration:2:9-11: Redeclaration of metric `foo' previously declared at duplicate declaration:1:9-11",
"duplicate declaration:1:9-11: Declaration of variable `foo' here is never used.",
},
},
{
"indexedExpr parameter count",
`counter n
counter foo by a, b
counter bar by a, b
counter quux by a
/(\d+)/ {
n[$1]++
foo[$1]++
bar[$1][0]++
quux[$1][0]++
}
`,
[]string{
// n[$1] is syntactically valid, but n is not indexable
"indexedExpr parameter count:6:7-10: Index taken on unindexable expression",
// foo[$1] is short one key
"indexedExpr parameter count:7:7-12: Not enough keys for indexed expression: expecting 2, received 1",
// bar[$1][0] is ok
// quux[$1][0] has too many keys
"indexedExpr parameter count:9:7-16: Too many keys for indexed expression: expecting 1, received 2.",
},
},
{
"indexedExpr binary expression",
`counter foo by a, b
counter bar by a, b
/(\d+)/ {
foo[$1]+=$1
}
/(.*)/ {
foo = bar[$1] + 1
}
`,
[]string{
"indexedExpr binary expression:4:3-8: Not enough keys for indexed expression: expecting 2, received 1",
"indexedExpr binary expression:7:3-5: Not enough keys for indexed expression: expecting 2, received 0",
"indexedExpr binary expression:7:9-14: Not enough keys for indexed expression: expecting 2, received 1",
},
},
{
"builtin parameter mismatch",
`/\d+/ {
strptime()
}
/\d+/ {
timestamp()
}
`,
[]string{"builtin parameter mismatch:2:4-13: call to `strptime': type mismatch; expected String→String→None received incomplete type"},
},
{
"bad strptime format",
`strptime("2017-10-16 06:50:25", "2017-10-16 06:50:25")
`,
[]string{
"bad strptime format:1:33-53: invalid time format string \"2017-10-16 06:50:25\"", "\tRefer to the documentation at https://golang.org/pkg/time/#pkg-constants for advice.",
},
},
{
"undefined const regex",
"/foo / + X + / bar/ {}\n",
[]string{"undefined const regex:1:10: Identifier `X' not declared.", "\tTry adding `const X /.../' earlier in the program."},
},
{
"unused symbols",
`counter foo
const ID /bar/
/asdf/ {
}
`,
[]string{
"unused symbols:1:9-11: Declaration of variable `foo' here is never used.",
"unused symbols:2:7-8: Declaration of named pattern constant `ID' here is never used.",
},
},
{
"invalid del index count",
`gauge t by x, y
/.*/ {
del t["x"]
t["x"]["y"]
}
`,
[]string{"invalid del index count:3:7-11: Not enough keys for indexed expression: expecting 2, received 1"},
},
// TODO(jaq): is it an error to make a counter of type string?
// {"counter as string",
// `counter foo
// /(?P<v>.*)/ {
// foo = $v
// }
// `,
// []string{"counter as string:4:4-11: Can't assign rhs of type String to lhs of type Int"}},
{
"def without usage",
`def x{next}`,
[]string{"def without usage:1:1-10: Declaration of decorator `x' here is never used."},
},
{
"def without next",
`def x{}
@x {
}`,
[]string{"def without next:1:1-3: No symbols found in decorator `@x'.", "\tTry adding a `next' statement inside the `{}' block."},
},
{
"def with two nexts",
`def x{
/a/ {
next
}
/b/ {
next
}
}
@x {
}`,
[]string{"def with two nexts:6:5-8: Can't use `next' statement twice in a decorator."},
},
{
"counter with buckets",
`counter foo buckets 1, 2, 3
/(\d)/ {
foo = $1
}`,
[]string{"counter with buckets:1:9-11: Can't specify buckets for non-histogram metric `foo'."},
},
{
"next outside of decorator",
`def x{
next
}
@x {
next
}
`,
[]string{"next outside of decorator:5:1-4: Can't use `next' outside of a decorator."},
},
{
"use decorator in decorator",
`def x {
@x {}
}`,
[]string{"use decorator in decorator:2:1-2: Decorator `@x' is not completely defined yet.", "\tTry removing @x from here.", "use decorator in decorator:2:1-2: No symbols found in decorator `@x'.", "\tTry adding a `next' statement inside the `{}' block."},
},
{
"delete incorrect object",
`/(.*)/ {
del $0
}`,
[]string{"delete incorrect object:2:5-6: Cannot delete this.", "\tTry deleting from a dimensioned metric with this as an index."},
},
{
"pattern fragment plus anything",
`gauge e
// + e {
}
`,
[]string{"pattern fragment plus anything:2:6: Can't append variable `e' to this pattern.", "\tTry using a `const'-defined pattern fragment."},
},
{
"recursive pattern fragment",
`const P//+P`,
[]string{"recursive pattern fragment:1:11: Can't evaluate pattern fragment `P' here.", "\tTry defining it earlier in the program."},
},
{
"delete a histogram",
`histogram#
m del#
m`,
[]string{"delete a histogram:3:7: Cannot delete this.", "\tTry deleting an index from this dimensioned metric."},
},
{
"int as bool",
`1 {}`,
[]string{"int as bool:1:1: Can't interpret Int as a boolean expression here.", "\tTry using comparison operators to make the condition explicit."},
},
{
"regexp too long",
"/" + strings.Repeat("c", 1025) + "/ {}",
[]string{"regexp too long:1:1-1027: Exceeded maximum regular expression pattern length of 1024 bytes with 1025.", "\tExcessively long patterns are likely to cause compilation and runtime performance problems."},
},
{
"strptime invalid args",
`strptime("",8)
`,
[]string{"strptime invalid args:1:13: Expecting a format string for argument 2 of strptime(), not Int."},
},
{
"inc invalid args",
`text l
l++
`,
[]string{"inc invalid args:2:1: type mismatch: expecting an Int for INC, not String."},
},
{
"mod by zero",
`2=9%0
`,
[]string{"mod by zero:1:3-5: Can't divide by zero."},
},
{
"assign to rvalue",
`gauge l
l++=l
`,
[]string{"assign to rvalue:2:1-3: Can't assign to expression on left; expecting a variable here."},
},
{
"tolower non string",
`tolower(2)
`,
[]string{"tolower non string:1:9: Expecting a String for argument 1 of tolower(), not Int."},
},
{
"dec non var",
`strptime("", "")--
`,
[]string{"dec non var:1:1-16: Can't assign to expression; expecting a variable here."},
},
// TODO(jaq): This is an instance of bug #190, the capref is ambiguous.
// {"regexp with no zero capref",
// `//||/;0/ {$0||// {}}
// `, []string{"regexp with no zero capref:1:5-6: Nonexistent capref =."}},
{
"cmp to None",
`strptime("","")<5{}
`,
[]string{"cmp to None:1:1-17: type mismatch: can't apply LT to LHS of type \"None\" with RHS of type \"Int\"."},
},
{
"negate None",
`~strptime("", "") {}
`,
[]string{"negate None:1:2-17: type mismatch; expected Int received None for `~' operator."},
},
}
func TestCheckInvalidPrograms(t *testing.T) {
for _, tc := range checkerInvalidPrograms {
tc := tc
t.Run(tc.name, func(t *testing.T) {
ast, err := parser.Parse(tc.name, strings.NewReader(tc.program))
testutil.FatalIfErr(t, err)
ast, err = checker.Check(ast, 0, 0)
if err == nil {
s := parser.Sexp{}
s.EmitTypes = true
t.Log(s.Dump(ast))
t.Fatal("check didn't fail")
}
if !testutil.ExpectNoDiff(t,
tc.errors, // want
strings.Split(err.Error(), "\n"), // got
cmpopts.SortSlices(func(x, y string) bool { return x < y })) {
t.Logf("Got: %s", err.Error())
s := parser.Sexp{}
s.EmitTypes = true
t.Log(s.Dump(ast))
}
})
}
}
var checkerValidPrograms = []struct {
name string
program string
}{
{
"capture group",
`counter foo
/(.*)/ {
foo += $1
}
`,
},
{
"shadowed positionals",
`counter foo
/(.*)/ {
foo += $1
/bar(\d+)/ {
foo += $1
}
}
`,
},
{
"sibling positionals",
`counter foo
/(.*)/ {
foo += $1
}
/bar(\d+)/ {
foo += $1
}
`,
},
{
"index expression",
`counter foo by a, b
/(\d)/ {
foo[1,$1] = 3
}`,
},
{
"odd indexes",
`counter foo by a,b,c
/(\d) (\d)/ {
foo[$1,$2][0]++
}
`,
},
{
"implicit int",
`counter foo
/$/ {
foo++
}`,
},
{
"function return value",
`len("foo") > 0 {}`,
},
{
"conversions",
`counter i
counter f
/(.*)/ {
i = int($1)
f = float($1)
}
`,
},
{
"logical operators",
`0 || 1 {
}
1 && 0 {
}
`,
},
{
"nested binary conditional",
`1 != 0 && 0 == 1 {
}
`,
},
{"paren expr", `
(0) || (1 && 3) {
}`},
{"strptime format", `
strptime("2006-01-02 15:04:05", "2006-01-02 15:04:05")
`},
{"string concat", `
counter f by s
/(.*), (.*)/ {
f[$1 + $2]++
}
`},
{"namespace", `
counter test
/(?P<test>.*)/ {
test++
}
`},
{"match expr 1", `
/(?P<foo>.*)/ {
$foo =~ /bar/ {
}
}`},
{"capref used in def", `
/(?P<x>\d+)/ && $x > 0 {
}`},
{"binop compare type conversion", `
gauge var
/(?P<x>\d+) (\d+\.\d+)/ {
var = $x + $2
}`},
{"binop arith type conversion", `
gauge var
/(?P<x>\d+) (\d+\.\d+)/ {
var = $x + $2
}`},
{"concat expr 1", `
const X /foo/
/bar/ + X {
}`},
{"concat expr 2", `
const X /foo/
X {
}`},
{"match expression 3", `
const X /foo/
"a" =~ X {
}
`},
{"match expr 4", `
/(?P<foo>.{6}) (?P<bar>.*)/ {
$foo =~ $bar {
}
}`},
{"decorator scopes", `
counter a
def decorator {
/(.).*/ {
next
}
}
@decorator {
$1 == "A" {
a++
}
}
`},
{"concat with add_assign", `
text foo
/(?P<v>.*)/ {
foo += $v
}
`},
{"decrement", `
counter i
/.*/ {
i--
}`},
{"stop", `
stop
// {
stop
}`},
{"declare histogram", `
histogram foo buckets 1, 2, 3
/(\d+)/ {
foo = $1
}`},
{"match a pattern in cond", `
const N /n/
N {
}`},
{"match a pattern in a binary expr in cond", `
const N /n/
N && 1 {
}`},
{"negative numbers in capture groups", `
gauge foo
/(?P<value_ms>-?\d+)/ {
foo += $value_ms / 1000.0
}`},
{"substitution", `
gauge foo
/(\d,\d)/ {
foo = subst(",", "", $1)
}`},
{"regexp subst", `
subst(/\d+/, "d", "1234")
`},
}
func TestCheckValidPrograms(t *testing.T) {
for _, tc := range checkerValidPrograms {
tc := tc
t.Run(tc.name, func(t *testing.T) {
ast, err := parser.Parse(tc.name, strings.NewReader(tc.program))
testutil.FatalIfErr(t, err)
ast, err = checker.Check(ast, 0, 0)
if *checkerTestDebug {
s := parser.Sexp{}
s.EmitTypes = true
t.Log("Typed AST:\n" + s.Dump(ast))
}
if err != nil {
t.Errorf("check failed: %s", err)
}
})
}
}
var checkerTypeExpressionTests = []struct {
name string
expr ast.Node
expected types.Type
}{
{
"Int + Int -> Int",
&ast.BinaryExpr{
LHS: &ast.IntLit{I: 1},
RHS: &ast.IntLit{I: 1},
Op: parser.PLUS,
},
types.Int,
},
{
"Int + Float -> Float",
&ast.BinaryExpr{
LHS: &ast.IntLit{I: 1},
RHS: &ast.FloatLit{F: 1.0},
Op: parser.PLUS,
},
types.Float,
},
{
" + Float -> Float",
&ast.BinaryExpr{
LHS: &ast.IDTerm{Symbol: &symbol.Symbol{Name: "i", Kind: symbol.VarSymbol, Type: types.NewVariable()}},
RHS: &ast.CaprefTerm{Symbol: &symbol.Symbol{Kind: symbol.CaprefSymbol, Type: types.Float}},
Op: parser.PLUS,
},
types.Float,
},
}
func TestCheckTypeExpressions(t *testing.T) {
for _, tc := range checkerTypeExpressionTests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
ast, err := checker.Check(tc.expr, 0, 0)
testutil.FatalIfErr(t, err)
if !testutil.ExpectNoDiff(t, tc.expected, ast.Type().Root()) {
s := parser.Sexp{}
s.EmitTypes = true
t.Log("Typed AST:\n" + s.Dump(ast))
}
})
}
}