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