github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/graphite/native/compiler_test.go (about) 1 // Copyright (c) 2019 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package native 22 23 import ( 24 "fmt" 25 "math" 26 "testing" 27 28 "github.com/m3db/m3/src/query/graphite/common" 29 "github.com/m3db/m3/src/query/graphite/lexer" 30 "github.com/m3db/m3/src/query/graphite/storage" 31 graphitetest "github.com/m3db/m3/src/query/graphite/testing" 32 "github.com/m3db/m3/src/query/graphite/ts" 33 xtest "github.com/m3db/m3/src/x/test" 34 35 "github.com/golang/mock/gomock" 36 "github.com/stretchr/testify/assert" 37 "github.com/stretchr/testify/require" 38 ) 39 40 type testCompile struct { 41 input string 42 result interface{} 43 } 44 45 func hello(ctx *common.Context) (string, error) { return "hello", nil } 46 func noArgs(ctx *common.Context) (ts.SeriesList, error) { return ts.SeriesList{}, nil } 47 func defaultArgs(ctx *common.Context, b bool, f1, f2 float64, s string) (ts.SeriesList, error) { 48 return ts.SeriesList{}, nil 49 } 50 51 func TestCompile1(t *testing.T) { 52 var ( 53 sortByName = findFunction("sortByName") 54 noArgs = findFunction("noArgs") 55 aliasByNode = findFunction("aliasByNode") 56 summarize = findFunction("summarize") 57 defaultArgs = findFunction("defaultArgs") 58 sumSeries = findFunction("sumSeries") 59 asPercent = findFunction("asPercent") 60 scale = findFunction("scale") 61 logarithm = findFunction("logarithm") 62 removeEmptySeries = findFunction("removeEmptySeries") 63 filterSeries = findFunction("filterSeries") 64 ) 65 66 tests := []testCompile{ 67 {"", noopExpression{}}, 68 {"foobar", newFetchExpression("foobar")}, 69 { 70 "foo.bar.{a,b,c}.baz-*.stat[0-9]", 71 newFetchExpression("foo.bar.{a,b,c}.baz-*.stat[0-9]"), 72 }, 73 {"noArgs()", &funcExpression{&functionCall{f: noArgs}}}, 74 {"sortByName(foo.bar.zed)", &funcExpression{ 75 &functionCall{ 76 f: sortByName, 77 in: []funcArg{ 78 newFetchExpression("foo.bar.zed"), 79 }, 80 }, 81 }}, 82 {"aliasByNode(foo.bar4.*.metrics.written, 2, 4)", &funcExpression{ 83 &functionCall{ 84 f: aliasByNode, 85 in: []funcArg{ 86 newFetchExpression("foo.bar4.*.metrics.written"), 87 newIntConst(2), 88 newIntConst(4), 89 }, 90 }, 91 }}, 92 {"summarize(foo.bar.baz.quux, \"1h\", \"max\", TRUE)", &funcExpression{ 93 &functionCall{ 94 f: summarize, 95 in: []funcArg{ 96 newFetchExpression("foo.bar.baz.quux"), 97 newStringConst("1h"), 98 newStringConst("max"), 99 newBoolConst(true), 100 }, 101 }, 102 }}, 103 {"summarize(foo.bar.baz.quuz, \"1h\")", &funcExpression{ 104 &functionCall{ 105 f: summarize, 106 in: []funcArg{ 107 newFetchExpression("foo.bar.baz.quuz"), 108 newStringConst("1h"), 109 newStringConst(""), 110 newBoolConst(false), 111 }, 112 }, 113 }}, 114 {"defaultArgs(true)", &funcExpression{ 115 &functionCall{ 116 f: defaultArgs, 117 in: []funcArg{ 118 newBoolConst(true), // non-default value 119 newFloat64Const(math.NaN()), // default value 120 newFloat64Const(100), // default value 121 newStringConst("foobar"), // default value 122 }, 123 }, 124 }}, 125 {"sortByName(aliasByNode(foo.bar72.*.metrics.written,2,4,6))", &funcExpression{ 126 &functionCall{ 127 f: sortByName, 128 in: []funcArg{ 129 &functionCall{ 130 f: aliasByNode, 131 in: []funcArg{ 132 newFetchExpression("foo.bar72.*.metrics.written"), 133 newIntConst(2), 134 newIntConst(4), 135 newIntConst(6), 136 }, 137 }, 138 }, 139 }, 140 }}, 141 {"sumSeries(foo.bar.baz.quux, foo.bar72.*.metrics.written)", &funcExpression{ 142 &functionCall{ 143 f: sumSeries, 144 in: []funcArg{ 145 newFetchExpression("foo.bar.baz.quux"), 146 newFetchExpression("foo.bar72.*.metrics.written"), 147 }, 148 }, 149 }}, 150 {"asPercent(foo.bar72.*.metrics.written, foo.bar.baz.quux)", &funcExpression{ 151 &functionCall{ 152 f: asPercent, 153 in: []funcArg{ 154 newFetchExpression("foo.bar72.*.metrics.written"), 155 newFetchExpression("foo.bar.baz.quux"), 156 }, 157 }, 158 }}, 159 {"asPercent(foo.bar72.*.metrics.written, sumSeries(foo.bar.baz.quux))", &funcExpression{ 160 &functionCall{ 161 f: asPercent, 162 in: []funcArg{ 163 newFetchExpression("foo.bar72.*.metrics.written"), 164 &functionCall{ 165 f: sumSeries, 166 in: []funcArg{ 167 newFetchExpression("foo.bar.baz.quux"), 168 }, 169 }, 170 }, 171 }, 172 }}, 173 {"asPercent(foo.bar72.*.metrics.written, 100)", &funcExpression{ 174 &functionCall{ 175 f: asPercent, 176 in: []funcArg{ 177 newFetchExpression("foo.bar72.*.metrics.written"), 178 newIntConst(100), 179 }, 180 }, 181 }}, 182 {"asPercent(foo.bar72.*.metrics.written)", &funcExpression{ 183 &functionCall{ 184 f: asPercent, 185 in: []funcArg{ 186 newFetchExpression("foo.bar72.*.metrics.written"), 187 newConstArg([]*ts.Series(nil)), 188 }, 189 }, 190 }}, 191 {"asPercent(foo.bar72.*.metrics.written, total=sumSeries(foo.bar.baz.quux))", &funcExpression{ 192 &functionCall{ 193 f: asPercent, 194 in: []funcArg{ 195 newFetchExpression("foo.bar72.*.metrics.written"), 196 &functionCall{ 197 f: sumSeries, 198 in: []funcArg{ 199 newFetchExpression("foo.bar.baz.quux"), 200 }, 201 }, 202 }, 203 }, 204 }}, 205 {"asPercent(foo.bar72.*.metrics.written, total=100)", &funcExpression{ 206 &functionCall{ 207 f: asPercent, 208 in: []funcArg{ 209 newFetchExpression("foo.bar72.*.metrics.written"), 210 newIntConst(100), 211 }, 212 }, 213 }}, 214 {"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1e+3)", &funcExpression{ 215 &functionCall{ 216 f: scale, 217 in: []funcArg{ 218 newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"), 219 newFloat64Const(1000), 220 }, 221 }, 222 }}, 223 {"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1e-3)", &funcExpression{ 224 &functionCall{ 225 f: scale, 226 in: []funcArg{ 227 newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"), 228 newFloat64Const(0.001), 229 }, 230 }, 231 }}, 232 {"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1e3)", &funcExpression{ 233 &functionCall{ 234 f: scale, 235 in: []funcArg{ 236 newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"), 237 newFloat64Const(1000), 238 }, 239 }, 240 }}, 241 {"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.1e3)", &funcExpression{ 242 &functionCall{ 243 f: scale, 244 in: []funcArg{ 245 newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"), 246 newFloat64Const(1100), 247 }, 248 }, 249 }}, 250 {"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.1e+3)", &funcExpression{ 251 &functionCall{ 252 f: scale, 253 in: []funcArg{ 254 newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"), 255 newFloat64Const(1100), 256 }, 257 }, 258 }}, 259 {"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.2e-3)", &funcExpression{ 260 &functionCall{ 261 f: scale, 262 in: []funcArg{ 263 newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"), 264 newFloat64Const(0.0012), 265 }, 266 }, 267 }}, 268 {"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, .1e+3)", &funcExpression{ 269 &functionCall{ 270 f: scale, 271 in: []funcArg{ 272 newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"), 273 newFloat64Const(100), 274 }, 275 }, 276 }}, 277 {"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 2.e+3)", &funcExpression{ 278 &functionCall{ 279 f: scale, 280 in: []funcArg{ 281 newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"), 282 newFloat64Const(2000), 283 }, 284 }, 285 }}, 286 {"logarithm(a.b.c)", &funcExpression{ 287 &functionCall{ 288 f: logarithm, 289 in: []funcArg{ 290 newFetchExpression("a.b.c"), 291 newFloat64Const(10), 292 }, 293 }, 294 }}, 295 {"removeEmptySeries(a.b.c)", &funcExpression{ 296 &functionCall{ 297 f: removeEmptySeries, 298 in: []funcArg{ 299 newFetchExpression("a.b.c"), 300 newFloat64Const(0), 301 }, 302 }, 303 }}, 304 {"filterSeries(a.b.c, 'max', '>', 1000)", &funcExpression{ 305 &functionCall{ 306 f: filterSeries, 307 in: []funcArg{ 308 newFetchExpression("a.b.c"), 309 newStringConst("max"), 310 newStringConst(">"), 311 newFloat64Const(1000), 312 }, 313 }, 314 }}, 315 } 316 317 ctrl := xtest.NewController(t) 318 defer ctrl.Finish() 319 320 ctx := common.NewTestContext() 321 store := storage.NewMockStorage(ctrl) 322 store.EXPECT().FetchByQuery(gomock.Any(), gomock.Any(), gomock.Any()). 323 Return(&storage.FetchResult{}, nil).AnyTimes() 324 ctx.Engine = NewEngine(store, CompileOptions{}) 325 326 for _, test := range tests { 327 expr, err := Compile(test.input, CompileOptions{}) 328 require.NoError(t, err, "error compiling: expression='%s', error='%v'", test.input, err) 329 require.NotNil(t, expr) 330 assertExprTree(t, test.result, expr, fmt.Sprintf("invalid result for %s: %v vs %v", 331 test.input, test.result, expr)) 332 333 // Ensure that the function can execute. 334 _, err = expr.Execute(ctx) 335 require.NoError(t, err) 336 } 337 } 338 339 type testCompilerError struct { 340 input string 341 err string 342 } 343 344 func TestCompileErrors(t *testing.T) { 345 tests := []testCompilerError{ 346 {"hello()", "top-level functions must return timeseries data"}, 347 {"foobar(", "invalid expression 'foobar(': could not find function named foobar"}, 348 {"foobar()", "invalid expression 'foobar()': could not find function named foobar"}, 349 {"sortByName(foo.*.zed)junk", "invalid expression 'sortByName(foo.*.zed)junk': " + 350 "extra data junk"}, 351 { 352 "aliasByNode(", 353 "invalid expression 'aliasByNode(': unexpected eof while parsing aliasByNode", 354 }, 355 { 356 "unknownFunc()", 357 "invalid expression 'unknownFunc()': could not find function named unknownFunc", 358 }, 359 { 360 "aliasByNode(10)", 361 "invalid expression 'aliasByNode(10)': invalid function call aliasByNode," + 362 " arg 0: expected a singlePathSpec, received a float64 '10'", 363 }, 364 { 365 "sortByName(hello())", 366 "invalid expression 'sortByName(hello())': invalid function call " + 367 "sortByName, arg 0: expected a singlePathSpec, received a functionCall 'hello()'", 368 }, 369 { 370 "aliasByNode()", 371 "invalid expression 'aliasByNode()': invalid number of arguments for aliasByNode;" + 372 " expected at least 2, received 0", 373 }, 374 { 375 "aliasByNode(foo.*.zed, 2, false)", 376 "invalid expression 'aliasByNode(foo.*.zed, 2, false)': invalid function call " + 377 "aliasByNode, arg 2: expected a int, received a bool 'false'", 378 }, 379 { 380 "aliasByNode(foo.*.bar,", 381 "invalid expression 'aliasByNode(foo.*.bar,': unexpected eof while" + 382 " parsing aliasByNode", 383 }, 384 { 385 "aliasByNode(foo.*.bar,)", 386 "invalid expression 'aliasByNode(foo.*.bar,)': invalid function call" + 387 " aliasByNode, arg 1: invalid expression 'aliasByNode(foo.*.bar,)': ) not valid", 388 }, 389 // TODO(jayp): Not providing all required parameters in a function with default parameters 390 // leads to an error message that states that a greater than required number of expected 391 // arguments. We could do better, but punting for now. 392 { 393 "summarize(foo.bar.baz.quux)", 394 "invalid expression 'summarize(foo.bar.baz.quux)':" + 395 " invalid number of arguments for summarize; expected 4, received 1", 396 }, 397 { 398 "sumSeries(foo.bar.baz.quux,)", 399 "invalid expression 'sumSeries(foo.bar.baz.quux,)': invalid function call sumSeries, " + 400 "arg 1: invalid expression 'sumSeries(foo.bar.baz.quux,)': ) not valid", 401 }, 402 { 403 "asPercent(foo.bar72.*.metrics.written, total", 404 "invalid expression 'asPercent(foo.bar72.*.metrics.written, total': " + 405 "invalid function call asPercent, " + 406 "arg 1: invalid expression 'asPercent(foo.bar72.*.metrics.written, total': " + 407 "unexpected eof, total should be followed by = or (", 408 }, 409 { 410 "asPercent(foo.bar72.*.metrics.written, total=", 411 "invalid expression 'asPercent(foo.bar72.*.metrics.written, total=': " + 412 "invalid function call asPercent, " + 413 "arg 1: invalid expression 'asPercent(foo.bar72.*.metrics.written, total=': " + 414 "unexpected eof, named argument total should be followed by its value", 415 }, 416 { 417 "asPercent(foo.bar72.*.metrics.written, total=randomStuff", 418 "invalid expression 'asPercent(foo.bar72.*.metrics.written, total=randomStuff': " + 419 "invalid function call asPercent, " + 420 "arg 1: invalid expression 'asPercent(foo.bar72.*.metrics.written, total=randomStuff': " + 421 "unexpected eof, randomStuff should be followed by = or (", 422 }, 423 { 424 "asPercent(foo.bar72.*.metrics.written, total=sumSeries(", 425 "invalid expression 'asPercent(foo.bar72.*.metrics.written, total=sumSeries(': " + 426 "invalid function call asPercent, " + 427 "arg 1: invalid expression 'asPercent(foo.bar72.*.metrics.written, total=sumSeries(': " + 428 "unexpected eof while parsing sumSeries", 429 }, 430 { 431 "scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.e)", 432 "invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.e)': " + 433 "invalid function call scale, " + 434 "arg 1: invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.e)': " + 435 "expected one of 0123456789, found ) not valid", 436 }, 437 { 438 "scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, .1e)", 439 "invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, .1e)': " + 440 "invalid function call scale, " + 441 "arg 1: invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, .1e)': " + 442 "expected one of 0123456789, found ) not valid", 443 }, 444 { 445 "scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, .e)", 446 "invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, .e)': " + 447 "invalid function call scale, " + 448 "arg 1: invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, .e)': " + 449 "expected one of 0123456789, found e not valid", 450 }, 451 { 452 "scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, e)", 453 "invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, e)': " + 454 "invalid function call scale, " + 455 "arg 1: expected a float64, received a fetchExpression 'fetch(e)'", 456 }, 457 { 458 "scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.2ee)", 459 "invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.2ee)': " + 460 "invalid function call scale, " + 461 "arg 1: invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.2ee)': " + 462 "expected one of 0123456789, found e not valid", 463 }, 464 } 465 466 for _, test := range tests { 467 expr, err := Compile(test.input, CompileOptions{}) 468 require.NotNil(t, err, "no error for %s", test.input) 469 assert.Equal(t, test.err, err.Error(), "wrong error for %s", test.input) 470 assert.Nil(t, expr, "non-nil expression for %s", test.input) 471 } 472 } 473 474 func assertExprTree(t *testing.T, expected interface{}, actual interface{}, msg string) { 475 switch e := expected.(type) { 476 case *functionCall: 477 a, ok := actual.(*functionCall) 478 require.True(t, ok, msg) 479 require.Equal(t, e.f.name, a.f.name, msg) 480 require.Equal(t, len(e.f.in), len(a.f.in), msg) 481 for i := range e.in { 482 assertExprTree(t, e.in[i], a.in[i], msg) 483 } 484 case noopExpression: 485 _, ok := actual.(noopExpression) 486 require.True(t, ok, msg) 487 case *funcExpression: 488 a, ok := actual.(*funcExpression) 489 require.True(t, ok, msg) 490 assertExprTree(t, e.call, a.call, msg) 491 case *fetchExpression: 492 a, ok := actual.(*fetchExpression) 493 require.True(t, ok, msg) 494 assert.Equal(t, e.pathArg.path, a.pathArg.path, msg) 495 case constFuncArg: 496 a, ok := actual.(constFuncArg) 497 require.True(t, ok, msg) 498 if !a.value.IsValid() { 499 // Explicit nil. 500 require.True(t, e.value.IsZero()) 501 } else { 502 graphitetest.Equalish(t, e.value.Interface(), a.value.Interface(), msg) 503 } 504 default: 505 assert.Equal(t, expected, actual, msg) 506 } 507 } 508 509 func TestExtractFetchExpressions(t *testing.T) { 510 tests := []struct { 511 expr string 512 targets []string 513 }{ 514 {"summarize(groupByNode(nonNegativeDerivative(foo.qaz.gauges.bar.baz.qux.foobar.*.quz.quail.count), 8, 'sum'), '10min', 'avg', true)", []string{ 515 "foo.qaz.gauges.bar.baz.qux.foobar.*.quz.quail.count", 516 }}, 517 {"asPercent(foo.bar72.*.metrics.written, total=sumSeries(foo.bar.baz.quux))", []string{ 518 "foo.bar72.*.metrics.written", "foo.bar.baz.quux", 519 }}, 520 {"foo.bar.{a,b,c}.baz-*.stat[0-9]", []string{ 521 "foo.bar.{a,b,c}.baz-*.stat[0-9]", 522 }}, 523 } 524 525 for _, test := range tests { 526 targets, err := ExtractFetchExpressions(test.expr) 527 require.NoError(t, err) 528 assert.Equal(t, test.targets, targets, test.expr) 529 } 530 } 531 532 func TestTokenLookforward(t *testing.T) { 533 tokenVals := []string{"a", "b", "c"} 534 tokens := make(chan *lexer.Token) 535 go func() { 536 for _, v := range tokenVals { 537 tokens <- lexer.MustMakeToken(v) 538 } 539 540 close(tokens) 541 }() 542 543 lookforward := newTokenLookforward(tokens) 544 token := lookforward.get() 545 assert.Equal(t, "a", token.Value()) 546 547 // assert that peek does not iterate token. 548 token, found := lookforward.peek() 549 assert.True(t, found) 550 assert.Equal(t, "b", token.Value()) 551 token, found = lookforward.peek() 552 assert.True(t, found) 553 assert.Equal(t, "b", token.Value()) 554 555 // assert that next get after peek will iterate forward. 556 token = lookforward.get() 557 assert.Equal(t, "b", token.Value()) 558 token = lookforward.get() 559 assert.Equal(t, "c", token.Value()) 560 561 // assert peek is empty once channel is closed. 562 _, found = lookforward.peek() 563 assert.False(t, found) 564 } 565 566 func init() { 567 MustRegisterFunction(noArgs) 568 MustRegisterFunction(hello) 569 MustRegisterFunction(defaultArgs).WithDefaultParams(map[uint8]interface{}{ 570 1: false, 571 2: math.NaN(), 572 3: 100.0, 573 4: "foobar", 574 }) 575 }