github.com/thanos-io/thanos@v0.32.5/pkg/query/test_test.go (about) 1 // Copyright (c) The Thanos Authors. 2 // Licensed under the Apache License 2.0. 3 4 package query 5 6 import ( 7 "context" 8 "fmt" 9 "math" 10 "os" 11 "path/filepath" 12 "regexp" 13 "strconv" 14 "strings" 15 "testing" 16 "time" 17 18 "github.com/pkg/errors" 19 "github.com/prometheus/common/model" 20 "github.com/prometheus/prometheus/model/labels" 21 "github.com/prometheus/prometheus/model/timestamp" 22 "github.com/prometheus/prometheus/promql" 23 "github.com/prometheus/prometheus/promql/parser" 24 "github.com/prometheus/prometheus/storage" 25 "github.com/prometheus/prometheus/util/teststorage" 26 ) 27 28 var ( 29 minNormal = math.Float64frombits(0x0010000000000000) // The smallest positive normal value of type float64. 30 31 patSpace = regexp.MustCompile("[\t ]+") 32 // TODO(bwplotka): Parse external labels. 33 patStore = regexp.MustCompile(`^store\s+([{}=_"a-zA-Z0-9]+)\s+([0-9mds]+)\s+([0-9mds]+)$`) 34 patLoad = regexp.MustCompile(`^load\s+(.+?)$`) 35 patEvalInstant = regexp.MustCompile(`^eval(?:_(fail|ordered))?\s+instant\s+(?:at\s+(.+?))?\s+(.+)$`) 36 ) 37 38 const ( 39 epsilon = 0.000001 // Relative error allowed for sample values. 40 ) 41 42 var testStartTime = time.Unix(0, 0).UTC() 43 44 func durationMilliseconds(d time.Duration) int64 { 45 return int64(d / (time.Millisecond / time.Nanosecond)) 46 } 47 48 func timeMilliseconds(t time.Time) int64 { 49 return t.UnixNano() / int64(time.Millisecond/time.Nanosecond) 50 } 51 52 type test struct { 53 testing.TB 54 55 cmds []interface{} 56 rootEngine *promql.Engine 57 stores []*testStore 58 59 ctx context.Context 60 cancelCtx context.CancelFunc 61 } 62 63 type testStore struct { 64 storeCmd 65 66 storage *teststorage.TestStorage 67 68 ctx context.Context 69 cancelCtx context.CancelFunc 70 } 71 72 func newTestStore(t testing.TB, cmd *storeCmd) *testStore { 73 s := &testStore{ 74 storeCmd: *cmd, 75 storage: teststorage.New(t), 76 } 77 s.ctx, s.cancelCtx = context.WithCancel(context.Background()) 78 return s 79 } 80 81 // close closes resources associated with the testStore. 82 func (s *testStore) close(t testing.TB) { 83 s.cancelCtx() 84 85 if err := s.storage.Close(); err != nil { 86 t.Fatalf("closing test storage: %s", err) 87 } 88 } 89 90 // NewTest returns an initialized empty Test. 91 // It's compatible with promql.Test, allowing additionally multi StoreAPIs for query pushdown testing. 92 // TODO(bwplotka): Move to unittest and add add support for multi-store upstream. See: https://github.com/prometheus/prometheus/pull/8300 93 func newTest(t testing.TB, input string) (*test, error) { 94 cmds, err := parse(input) 95 if err != nil { 96 return nil, err 97 } 98 99 te := &test{TB: t, cmds: cmds} 100 te.reset() 101 return te, err 102 } 103 104 func newTestFromFile(t testing.TB, filename string) (*test, error) { 105 content, err := os.ReadFile(filepath.Clean(filename)) 106 if err != nil { 107 return nil, err 108 } 109 return newTest(t, string(content)) 110 } 111 112 // reset the current test storage of all inserted samples. 113 func (t *test) reset() { 114 if t.cancelCtx != nil { 115 t.cancelCtx() 116 } 117 t.ctx, t.cancelCtx = context.WithCancel(context.Background()) 118 119 opts := promql.EngineOpts{ 120 Logger: nil, 121 Reg: nil, 122 MaxSamples: 10000, 123 Timeout: 100 * time.Second, 124 NoStepSubqueryIntervalFn: func(int64) int64 { return durationMilliseconds(1 * time.Minute) }, 125 } 126 t.rootEngine = promql.NewEngine(opts) 127 128 for _, s := range t.stores { 129 s.close(t.TB) 130 } 131 t.stores = t.stores[:0] 132 } 133 134 // close closes resources associated with the Test. 135 func (t *test) close() { 136 t.cancelCtx() 137 for _, s := range t.stores { 138 s.close(t.TB) 139 } 140 } 141 142 // getLines returns trimmed lines after removing the comments. 143 func getLines(input string) []string { 144 lines := strings.Split(input, "\n") 145 for i, l := range lines { 146 l = strings.TrimSpace(l) 147 if strings.HasPrefix(l, "#") { 148 l = "" 149 } 150 lines[i] = l 151 } 152 return lines 153 } 154 155 // parse parses the given input and returns command sequence. 156 func parse(input string) (cmds []interface{}, err error) { 157 lines := getLines(input) 158 159 // Scan for steps line by line. 160 for i := 0; i < len(lines); i++ { 161 l := lines[i] 162 if l == "" { 163 continue 164 } 165 var cmd interface{} 166 167 switch c := strings.ToLower(patSpace.Split(l, 2)[0]); { 168 case c == "clear": 169 cmd = &clearCmd{} 170 case c == "load": 171 i, cmd, err = ParseLoad(lines, i) 172 case strings.HasPrefix(c, "eval"): 173 i, cmd, err = ParseEval(lines, i) 174 case c == "store": 175 i, cmd, err = ParseStore(lines, i) 176 default: 177 return nil, raise(i, "invalid command %q", l) 178 } 179 if err != nil { 180 return nil, err 181 } 182 cmds = append(cmds, cmd) 183 } 184 return cmds, nil 185 } 186 187 func raise(line int, format string, v ...interface{}) error { 188 return &parser.ParseErr{ 189 LineOffset: line, 190 Err: errors.Errorf(format, v...), 191 } 192 } 193 194 // run executes the command sequence of the test. Until the maximum error number 195 // is reached, evaluation errors do not terminate execution. 196 func (t *test) run(createQueryableFn func([]*testStore) storage.Queryable) error { 197 for _, cmd := range t.cmds { 198 if err := t.exec(cmd, createQueryableFn); err != nil { 199 return err 200 } 201 } 202 return nil 203 } 204 205 // exec processes a single step of the test. 206 func (t *test) exec(tc interface{}, createQueryableFn func([]*testStore) storage.Queryable) error { 207 switch cmd := tc.(type) { 208 case *clearCmd: 209 t.reset() 210 case *storeCmd: 211 t.stores = append(t.stores, newTestStore(t.TB, tc.(*storeCmd))) 212 213 case *loadCmd: 214 if len(t.stores) == 0 { 215 t.stores = append(t.stores, newTestStore(t.TB, newStoreCmd(nil, math.MinInt64, math.MaxInt64))) 216 } 217 218 app := t.stores[len(t.stores)-1].storage.Appender(t.ctx) 219 if err := cmd.Append(app); err != nil { 220 _ = app.Rollback() 221 return err 222 } 223 if err := app.Commit(); err != nil { 224 return err 225 } 226 227 case *evalCmd: 228 if err := cmd.Eval(t.ctx, t.rootEngine, createQueryableFn(t.stores)); err != nil { 229 return err 230 } 231 232 default: 233 return errors.Errorf("pkg/query.Test.exec: unknown test command type %v", cmd) 234 } 235 return nil 236 } 237 238 // storeCmd is a command that appends new storage with filter. 239 type storeCmd struct { 240 matchers []*labels.Matcher 241 mint, maxt int64 242 } 243 244 func newStoreCmd(matchers []*labels.Matcher, mint, maxt int64) *storeCmd { 245 return &storeCmd{ 246 matchers: matchers, 247 mint: mint, 248 maxt: maxt, 249 } 250 } 251 252 func (cmd storeCmd) String() string { 253 return "store" 254 } 255 256 // ParseStore parses store statements. 257 func ParseStore(lines []string, i int) (int, *storeCmd, error) { 258 if !patStore.MatchString(lines[i]) { 259 return i, nil, raise(i, "invalid store command. (store <matchers> <mint offset> <maxt offset>)") 260 } 261 parts := patStore.FindStringSubmatch(lines[i]) 262 263 m, err := parser.ParseMetricSelector(parts[1]) 264 if err != nil { 265 return i, nil, raise(i, "invalid matcher definition %q: %s", parts[1], err) 266 } 267 268 offset, err := model.ParseDuration(parts[2]) 269 if err != nil { 270 return i, nil, raise(i, "invalid mint definition %q: %s", parts[2], err) 271 } 272 mint := testStartTime.Add(time.Duration(offset)) 273 274 offset, err = model.ParseDuration(parts[3]) 275 if err != nil { 276 return i, nil, raise(i, "invalid maxt definition %q: %s", parts[3], err) 277 } 278 maxt := testStartTime.Add(time.Duration(offset)) 279 return i, newStoreCmd(m, timestamp.FromTime(mint), timestamp.FromTime(maxt)), nil 280 } 281 282 // ParseLoad parses load statements. 283 func ParseLoad(lines []string, i int) (int, *loadCmd, error) { 284 if !patLoad.MatchString(lines[i]) { 285 return i, nil, raise(i, "invalid load command. (load <step:duration>)") 286 } 287 parts := patLoad.FindStringSubmatch(lines[i]) 288 289 gap, err := model.ParseDuration(parts[1]) 290 if err != nil { 291 return i, nil, raise(i, "invalid step definition %q: %s", parts[1], err) 292 } 293 cmd := newLoadCmd(time.Duration(gap)) 294 for i+1 < len(lines) { 295 i++ 296 defLine := lines[i] 297 if defLine == "" { 298 i-- 299 break 300 } 301 metric, vals, err := parser.ParseSeriesDesc(defLine) 302 if err != nil { 303 if perr, ok := err.(*parser.ParseErr); ok { 304 perr.LineOffset = i 305 } 306 return i, nil, err 307 } 308 cmd.set(metric, vals...) 309 } 310 return i, cmd, nil 311 } 312 313 // ParseEval parses eval statements. 314 func ParseEval(lines []string, i int) (int, *evalCmd, error) { 315 if !patEvalInstant.MatchString(lines[i]) { 316 return i, nil, raise(i, "invalid evaluation command. (eval[_fail|_ordered] instant [at <offset:duration>] <query>") 317 } 318 parts := patEvalInstant.FindStringSubmatch(lines[i]) 319 var ( 320 mod = parts[1] 321 at = parts[2] 322 expr = parts[3] 323 ) 324 _, err := parser.ParseExpr(expr) 325 if err != nil { 326 if perr, ok := err.(*parser.ParseErr); ok { 327 perr.LineOffset = i 328 posOffset := parser.Pos(strings.Index(lines[i], expr)) 329 perr.PositionRange.Start += posOffset 330 perr.PositionRange.End += posOffset 331 perr.Query = lines[i] 332 } 333 return i, nil, err 334 } 335 336 offset, err := model.ParseDuration(at) 337 if err != nil { 338 return i, nil, raise(i, "invalid step definition %q: %s", parts[1], err) 339 } 340 ts := testStartTime.Add(time.Duration(offset)) 341 342 cmd := newEvalCmd(expr, ts, i+1) 343 switch mod { 344 case "ordered": 345 cmd.ordered = true 346 case "fail": 347 cmd.fail = true 348 } 349 350 for j := 1; i+1 < len(lines); j++ { 351 i++ 352 defLine := lines[i] 353 if defLine == "" { 354 i-- 355 break 356 } 357 if f, err := parseNumber(defLine); err == nil { 358 cmd.expect(0, nil, parser.SequenceValue{Value: f}) 359 break 360 } 361 metric, vals, err := parser.ParseSeriesDesc(defLine) 362 if err != nil { 363 if perr, ok := err.(*parser.ParseErr); ok { 364 perr.LineOffset = i 365 } 366 return i, nil, err 367 } 368 369 // Currently, we are not expecting any matrices. 370 if len(vals) > 1 { 371 return i, nil, raise(i, "expecting multiple values in instant evaluation not allowed") 372 } 373 cmd.expect(j, metric, vals...) 374 } 375 return i, cmd, nil 376 } 377 378 func parseNumber(s string) (float64, error) { 379 n, err := strconv.ParseInt(s, 0, 64) 380 f := float64(n) 381 if err != nil { 382 f, err = strconv.ParseFloat(s, 64) 383 } 384 if err != nil { 385 return 0, errors.Wrap(err, "error parsing number") 386 } 387 return f, nil 388 } 389 390 // loadCmd is a command that loads sequences of sample values for specific 391 // metrics into the storage. 392 type loadCmd struct { 393 gap time.Duration 394 metrics map[uint64]labels.Labels 395 defs map[uint64][]promql.FPoint 396 } 397 398 func newLoadCmd(gap time.Duration) *loadCmd { 399 return &loadCmd{ 400 gap: gap, 401 metrics: map[uint64]labels.Labels{}, 402 defs: map[uint64][]promql.FPoint{}, 403 } 404 } 405 406 func (cmd loadCmd) String() string { 407 return "load" 408 } 409 410 // set a sequence of sample values for the given metric. 411 func (cmd *loadCmd) set(m labels.Labels, vals ...parser.SequenceValue) { 412 h := m.Hash() 413 414 samples := make([]promql.FPoint, 0, len(vals)) 415 ts := testStartTime 416 for _, v := range vals { 417 if !v.Omitted { 418 samples = append(samples, promql.FPoint{ 419 T: ts.UnixNano() / int64(time.Millisecond/time.Nanosecond), 420 F: v.Value, 421 }) 422 } 423 ts = ts.Add(cmd.gap) 424 } 425 cmd.defs[h] = samples 426 cmd.metrics[h] = m 427 } 428 429 // Append the defined time series to the storage. 430 func (cmd *loadCmd) Append(a storage.Appender) error { 431 for h, smpls := range cmd.defs { 432 m := cmd.metrics[h] 433 434 for _, s := range smpls { 435 if _, err := a.Append(0, m, s.T, s.F); err != nil { 436 return err 437 } 438 } 439 } 440 return nil 441 } 442 443 // evalCmd is a command that evaluates an expression for the given time (range) 444 // and expects a specific result. 445 type evalCmd struct { 446 expr string 447 start time.Time 448 line int 449 450 fail, ordered bool 451 452 metrics map[uint64]labels.Labels 453 expected map[uint64]entry 454 } 455 456 type entry struct { 457 pos int 458 vals []parser.SequenceValue 459 } 460 461 func (e entry) String() string { 462 return fmt.Sprintf("%d: %s", e.pos, e.vals) 463 } 464 465 func newEvalCmd(expr string, start time.Time, line int) *evalCmd { 466 return &evalCmd{ 467 expr: expr, 468 start: start, 469 line: line, 470 471 metrics: map[uint64]labels.Labels{}, 472 expected: map[uint64]entry{}, 473 } 474 } 475 476 func (ev *evalCmd) String() string { 477 return "eval" 478 } 479 480 // expect adds a new metric with a sequence of values to the set of expected 481 // results for the query. 482 func (ev *evalCmd) expect(pos int, m labels.Labels, vals ...parser.SequenceValue) { 483 if m == nil { 484 ev.expected[0] = entry{pos: pos, vals: vals} 485 return 486 } 487 h := m.Hash() 488 ev.metrics[h] = m 489 ev.expected[h] = entry{pos: pos, vals: vals} 490 } 491 492 // samplesAlmostEqual returns true if the two sample lines only differ by a 493 // small relative error in their sample value. 494 func almostEqual(a, b float64) bool { 495 // NaN has no equality but for testing we still want to know whether both values 496 // are NaN. 497 if math.IsNaN(a) && math.IsNaN(b) { 498 return true 499 } 500 501 // Cf. http://floating-point-gui.de/errors/comparison/ 502 if a == b { 503 return true 504 } 505 506 diff := math.Abs(a - b) 507 508 if a == 0 || b == 0 || diff < minNormal { 509 return diff < epsilon*minNormal 510 } 511 return diff/(math.Abs(a)+math.Abs(b)) < epsilon 512 } 513 514 // compareResult compares the result value with the defined expectation. 515 func (ev *evalCmd) compareResult(result parser.Value) error { 516 switch val := result.(type) { 517 case promql.Matrix: 518 return errors.New("received range result on instant evaluation") 519 520 case promql.Vector: 521 seen := map[uint64]bool{} 522 for pos, v := range val { 523 fp := v.Metric.Hash() 524 if _, ok := ev.metrics[fp]; !ok { 525 return errors.Errorf("unexpected metric %s in result", v.Metric) 526 } 527 exp := ev.expected[fp] 528 if ev.ordered && exp.pos != pos+1 { 529 return errors.Errorf("expected metric %s with %v at position %d but was at %d", v.Metric, exp.vals, exp.pos, pos+1) 530 } 531 if !almostEqual(exp.vals[0].Value, v.F) { 532 return errors.Errorf("expected %v for %s but got %v", exp.vals[0].Value, v.Metric, v.F) 533 } 534 535 seen[fp] = true 536 } 537 for fp, expVals := range ev.expected { 538 if !seen[fp] { 539 details := fmt.Sprintln("vector result", len(val), ev.expr) 540 for _, ss := range val { 541 details += fmt.Sprintln(" ", ss.Metric, ss.T, ss.F) 542 } 543 return errors.Errorf("expected metric %s with %v not found; details: %v", ev.metrics[fp], expVals, details) 544 } 545 } 546 547 case promql.Scalar: 548 if !almostEqual(ev.expected[0].vals[0].Value, val.V) { 549 return errors.Errorf("expected Scalar %v but got %v", val.V, ev.expected[0].vals[0].Value) 550 } 551 552 default: 553 panic(errors.Errorf("promql.Test.compareResult: unexpected result type %T", result)) 554 } 555 return nil 556 } 557 558 func (ev *evalCmd) Eval(ctx context.Context, queryEngine *promql.Engine, queryable storage.Queryable) error { 559 q, err := queryEngine.NewInstantQuery(ctx, queryable, promql.NewPrometheusQueryOpts(false, 0), ev.expr, ev.start) 560 if err != nil { 561 return err 562 } 563 defer q.Close() 564 565 res := q.Exec(ctx) 566 if res.Err != nil { 567 if ev.fail { 568 return nil 569 } 570 return errors.Wrapf(res.Err, "error evaluating query %q (line %d)", ev.expr, ev.line) 571 } 572 if res.Err == nil && ev.fail { 573 return errors.Errorf("expected error evaluating query %q (line %d) but got none", ev.expr, ev.line) 574 } 575 576 err = ev.compareResult(res.Value) 577 if err != nil { 578 return errors.Wrapf(err, "error in %s %s", ev, ev.expr) 579 } 580 581 // Check query returns same result in range mode, 582 // by checking against the middle step. 583 q, err = queryEngine.NewRangeQuery(ctx, queryable, promql.NewPrometheusQueryOpts(false, 0), ev.expr, ev.start.Add(-time.Minute), ev.start.Add(time.Minute), time.Minute) 584 if err != nil { 585 return err 586 } 587 rangeRes := q.Exec(ctx) 588 if rangeRes.Err != nil { 589 return errors.Wrapf(rangeRes.Err, "error evaluating query %q (line %d) in range mode", ev.expr, ev.line) 590 } 591 defer q.Close() 592 if ev.ordered { 593 // Ordering isn't defined for range queries. 594 return nil 595 } 596 mat := rangeRes.Value.(promql.Matrix) 597 vec := make(promql.Vector, 0, len(mat)) 598 for _, series := range mat { 599 for _, point := range series.Floats { 600 if point.T == timeMilliseconds(ev.start) { 601 vec = append(vec, promql.Sample{Metric: series.Metric, T: point.T, F: point.F}) 602 break 603 } 604 } 605 } 606 if _, ok := res.Value.(promql.Scalar); ok { 607 err = ev.compareResult(promql.Scalar{V: vec[0].F}) 608 } else { 609 err = ev.compareResult(vec) 610 } 611 if err != nil { 612 return errors.Wrapf(err, "error in %s %s (line %d) rande mode", ev, ev.expr, ev.line) 613 } 614 return nil 615 } 616 617 // clearCmd is a command that wipes the test's storage state. 618 type clearCmd struct{} 619 620 func (cmd clearCmd) String() string { 621 return "clear" 622 }