github.com/m3db/m3@v1.5.0/src/query/test/compatibility/test.go (about) 1 // Copyright (c) 2020 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 // Copyright 2015 The Prometheus Authors 22 // Licensed under the Apache License, Version 2.0 (the "License"); 23 // you may not use this file except in compliance with the License. 24 // You may obtain a copy of the License at 25 // 26 // http://www.apache.org/licenses/LICENSE-2.0 27 // 28 // Unless required by applicable law or agreed to in writing, software 29 // distributed under the License is distributed on an "AS IS" BASIS, 30 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31 // See the License for the specific language governing permissions and 32 // limitations under the License. 33 34 // Parts of code were taken from prometheus repo: https://github.com/prometheus/prometheus/blob/master/promql/test.go 35 36 package compatibility 37 38 import ( 39 "context" 40 "encoding/json" 41 "fmt" 42 "io/ioutil" 43 "math" 44 "regexp" 45 "sort" 46 "strconv" 47 "strings" 48 "time" 49 50 cparser "github.com/m3db/m3/src/cmd/services/m3comparator/main/parser" 51 "github.com/m3db/m3/src/query/api/v1/handler/prometheus" 52 53 "github.com/pkg/errors" 54 "github.com/prometheus/common/model" 55 "github.com/prometheus/prometheus/pkg/labels" 56 "github.com/prometheus/prometheus/promql" 57 "github.com/prometheus/prometheus/promql/parser" 58 "github.com/prometheus/prometheus/util/testutil" 59 ) 60 61 var ( 62 minNormal = math.Float64frombits(0x0010000000000000) // The smallest positive normal value of type float64. 63 64 patSpace = regexp.MustCompile("[\t ]+") 65 patLoad = regexp.MustCompile(`^load\s+(.+?)$`) 66 patEvalInstant = regexp.MustCompile(`^eval(?:_(fail|ordered))?\s+instant\s+(?:at\s+(.+?))?\s+(.+)$`) 67 ) 68 69 const ( 70 epsilon = 0.000001 // Relative error allowed for sample values. 71 startingTime = 1587393285000000000 // 2020-04-20 17:34:45 72 ) 73 74 var testStartTime = time.Unix(0, 0).UTC() 75 76 // Test is a sequence of read and write commands that are run 77 // against a test storage. 78 type Test struct { 79 testutil.T 80 81 cmds []testCommand 82 83 context context.Context 84 85 m3comparator *m3comparatorClient 86 } 87 88 // NewTest returns an initialized empty Test. 89 func NewTest(t testutil.T, input string) (*Test, error) { 90 test := &Test{ 91 T: t, 92 cmds: []testCommand{}, 93 m3comparator: newM3ComparatorClient("localhost", 9001), 94 } 95 err := test.parse(input) 96 if err != nil { 97 return test, err 98 } 99 err = test.clear() 100 return test, err 101 } 102 103 func newTestFromFile(t testutil.T, filename string) (*Test, error) { 104 content, err := ioutil.ReadFile(filename) 105 if err != nil { 106 return nil, err 107 } 108 return NewTest(t, string(content)) 109 } 110 111 func raise(line int, format string, v ...interface{}) error { 112 return &parser.ParseErr{ 113 LineOffset: line, 114 Err: errors.Errorf(format, v...), 115 } 116 } 117 118 func (t *Test) parseLoad(lines []string, i int) (int, *loadCmd, error) { 119 if !patLoad.MatchString(lines[i]) { 120 return i, nil, raise(i, "invalid load command. (load <step:duration>)") 121 } 122 parts := patLoad.FindStringSubmatch(lines[i]) 123 124 gap, err := model.ParseDuration(parts[1]) 125 if err != nil { 126 return i, nil, raise(i, "invalid step definition %q: %s", parts[1], err) 127 } 128 cmd := newLoadCmd(t.m3comparator, time.Duration(gap)) 129 for i+1 < len(lines) { 130 i++ 131 defLine := lines[i] 132 if len(defLine) == 0 { 133 i-- 134 break 135 } 136 metric, vals, err := parser.ParseSeriesDesc(defLine) 137 if err != nil { 138 if perr, ok := err.(*parser.ParseErr); ok { 139 perr.LineOffset = i 140 } 141 return i, nil, err 142 } 143 cmd.set(metric, vals...) 144 } 145 return i, cmd, nil 146 } 147 148 func (t *Test) parseEval(lines []string, i int) (int, *evalCmd, error) { 149 if !patEvalInstant.MatchString(lines[i]) { 150 return i, nil, raise(i, "invalid evaluation command. (eval[_fail|_ordered] instant [at <offset:duration>] <query>") 151 } 152 parts := patEvalInstant.FindStringSubmatch(lines[i]) 153 var ( 154 mod = parts[1] 155 at = parts[2] 156 expr = parts[3] 157 ) 158 _, err := parser.ParseExpr(expr) 159 if err != nil { 160 if perr, ok := err.(*parser.ParseErr); ok { 161 perr.LineOffset = i 162 posOffset := parser.Pos(strings.Index(lines[i], expr)) 163 perr.PositionRange.Start += posOffset 164 perr.PositionRange.End += posOffset 165 perr.Query = lines[i] 166 } 167 return i, nil, err 168 } 169 170 offset, err := model.ParseDuration(at) 171 if err != nil { 172 return i, nil, raise(i, "invalid step definition %q: %s", parts[1], err) 173 } 174 ts := testStartTime.Add(time.Duration(offset)) 175 176 cmd := newEvalCmd(expr, ts, i+1) 177 switch mod { 178 case "ordered": 179 cmd.ordered = true 180 case "fail": 181 cmd.fail = true 182 } 183 184 for j := 1; i+1 < len(lines); j++ { 185 i++ 186 defLine := lines[i] 187 if len(defLine) == 0 { 188 i-- 189 break 190 } 191 if f, err := parseNumber(defLine); err == nil { 192 cmd.expect(0, nil, parser.SequenceValue{Value: f}) 193 break 194 } 195 metric, vals, err := parser.ParseSeriesDesc(defLine) 196 if err != nil { 197 if perr, ok := err.(*parser.ParseErr); ok { 198 perr.LineOffset = i 199 } 200 return i, nil, err 201 } 202 203 // Currently, we are not expecting any matrices. 204 if len(vals) > 1 { 205 return i, nil, raise(i, "expecting multiple values in instant evaluation not allowed") 206 } 207 cmd.expect(j, metric, vals...) 208 } 209 return i, cmd, nil 210 } 211 212 // getLines returns trimmed lines after removing the comments. 213 func getLines(input string) []string { 214 lines := strings.Split(input, "\n") 215 for i, l := range lines { 216 l = strings.TrimSpace(l) 217 if strings.HasPrefix(l, "#") { 218 l = "" 219 } 220 lines[i] = l 221 } 222 return lines 223 } 224 225 // parse the given command sequence and appends it to the test. 226 func (t *Test) parse(input string) error { 227 lines := getLines(input) 228 var err error 229 // Scan for steps line by line. 230 for i := 0; i < len(lines); i++ { 231 l := lines[i] 232 if len(l) == 0 { 233 continue 234 } 235 var cmd testCommand 236 237 switch c := strings.ToLower(patSpace.Split(l, 2)[0]); { 238 case c == "clear": 239 cmd = &clearCmd{} 240 case c == "load": 241 i, cmd, err = t.parseLoad(lines, i) 242 case strings.HasPrefix(c, "eval"): 243 i, cmd, err = t.parseEval(lines, i) 244 default: 245 return raise(i, "invalid command %q", l) 246 } 247 if err != nil { 248 return err 249 } 250 t.cmds = append(t.cmds, cmd) 251 } 252 return nil 253 } 254 255 // testCommand is an interface that ensures that only the package internal 256 // types can be a valid command for a test. 257 type testCommand interface { 258 testCmd() 259 } 260 261 func (*clearCmd) testCmd() {} 262 func (*loadCmd) testCmd() {} 263 func (*evalCmd) testCmd() {} 264 265 // loadCmd is a command that loads sequences of sample values for specific 266 // metrics into the storage. 267 type loadCmd struct { 268 gap time.Duration 269 metrics map[uint64]labels.Labels 270 defs map[uint64][]promql.Point 271 m3compClient *m3comparatorClient 272 } 273 274 func newLoadCmd(m3compClient *m3comparatorClient, gap time.Duration) *loadCmd { 275 return &loadCmd{ 276 gap: gap, 277 metrics: map[uint64]labels.Labels{}, 278 defs: map[uint64][]promql.Point{}, 279 m3compClient: m3compClient, 280 } 281 } 282 283 func (cmd loadCmd) String() string { 284 return "load" 285 } 286 287 // set a sequence of sample values for the given metric. 288 func (cmd *loadCmd) set(m labels.Labels, vals ...parser.SequenceValue) { 289 h := m.Hash() 290 291 samples := make([]promql.Point, 0, len(vals)) 292 ts := testStartTime 293 for _, v := range vals { 294 if !v.Omitted { 295 samples = append(samples, promql.Point{ 296 T: ts.UnixNano() / int64(time.Millisecond/time.Nanosecond), 297 V: v.Value, 298 }) 299 } 300 ts = ts.Add(cmd.gap) 301 } 302 cmd.defs[h] = samples 303 cmd.metrics[h] = m 304 } 305 306 // append the defined time series to the storage. 307 func (cmd *loadCmd) append() error { 308 series := make([]cparser.Series, 0, len(cmd.defs)) 309 310 for h, smpls := range cmd.defs { 311 m := cmd.metrics[h] 312 start := time.Unix(0, startingTime) 313 314 ser := cparser.Series{ 315 Tags: make(cparser.Tags, 0, len(m)), 316 Start: start, 317 Datapoints: make(cparser.Datapoints, 0, len(smpls)), 318 } 319 for _, l := range m { 320 ser.Tags = append(ser.Tags, cparser.NewTag(l.Name, l.Value)) 321 } 322 323 for _, s := range smpls { 324 ts := start.Add(time.Duration(s.T) * time.Millisecond) 325 ser.Datapoints = append(ser.Datapoints, cparser.Datapoint{ 326 Timestamp: ts, 327 Value: cparser.Value(s.V), 328 }) 329 330 ser.End = ts.Add(cmd.gap * time.Millisecond) 331 } 332 series = append(series, ser) 333 } 334 335 j, err := json.Marshal(series) 336 if err != nil { 337 return err 338 } 339 340 return cmd.m3compClient.load(j) 341 } 342 343 // evalCmd is a command that evaluates an expression for the given time (range) 344 // and expects a specific result. 345 type evalCmd struct { 346 expr string 347 start time.Time 348 line int 349 350 fail, ordered bool 351 352 metrics map[uint64]labels.Labels 353 expected map[uint64]entry 354 m3query *m3queryClient 355 } 356 357 type entry struct { 358 pos int 359 vals []parser.SequenceValue 360 } 361 362 func (e entry) String() string { 363 return fmt.Sprintf("%d: %s", e.pos, e.vals) 364 } 365 366 func newEvalCmd(expr string, start time.Time, line int) *evalCmd { 367 return &evalCmd{ 368 expr: expr, 369 start: start, 370 line: line, 371 372 metrics: map[uint64]labels.Labels{}, 373 expected: map[uint64]entry{}, 374 m3query: newM3QueryClient("localhost", 7201), 375 } 376 } 377 378 func (ev *evalCmd) String() string { 379 return "eval" 380 } 381 382 // expect adds a new metric with a sequence of values to the set of expected 383 // results for the query. 384 func (ev *evalCmd) expect(pos int, m labels.Labels, vals ...parser.SequenceValue) { 385 if m == nil { 386 ev.expected[0] = entry{pos: pos, vals: vals} 387 return 388 } 389 h := m.Hash() 390 ev.metrics[h] = m 391 ev.expected[h] = entry{pos: pos, vals: vals} 392 } 393 394 // Hash returns a hash value for the label set. 395 func hash(ls prometheus.Tags) uint64 { 396 lbs := make(labels.Labels, 0, len(ls)) 397 for k, v := range ls { 398 lbs = append(lbs, labels.Label{ 399 Name: k, 400 Value: v, 401 }) 402 } 403 404 sort.Slice(lbs[:], func(i, j int) bool { 405 return lbs[i].Name < lbs[j].Name 406 }) 407 408 return lbs.Hash() 409 } 410 411 // compareResult compares the result value with the defined expectation. 412 func (ev *evalCmd) compareResult(j []byte) error { 413 var response prometheus.Response 414 err := json.Unmarshal(j, &response) 415 if err != nil { 416 return err 417 } 418 419 if response.Status != "success" { 420 return fmt.Errorf("unsuccess status received: %s", response.Status) 421 } 422 423 result := response.Data.Result 424 425 switch result := result.(type) { 426 case *prometheus.MatrixResult: 427 return errors.New("received range result on instant evaluation") 428 429 case *prometheus.VectorResult: 430 seen := map[uint64]bool{} 431 for pos, v := range result.Result { 432 fp := hash(v.Metric) 433 if _, ok := ev.metrics[fp]; !ok { 434 return errors.Errorf("unexpected metric %s in result", v.Metric) 435 } 436 437 exp := ev.expected[fp] 438 if ev.ordered && exp.pos != pos+1 { 439 return errors.Errorf("expected metric %s with %v at position %d but was at %d", v.Metric, exp.vals, exp.pos, pos+1) 440 } 441 val, err := parseNumber(fmt.Sprint(v.Value[1])) 442 if err != nil { 443 return err 444 } 445 if !almostEqual(exp.vals[0].Value, val) { 446 return errors.Errorf("expected %v for %s but got %v", exp.vals[0].Value, v.Metric, val) 447 } 448 seen[fp] = true 449 } 450 451 for fp, expVals := range ev.expected { 452 if !seen[fp] { 453 fmt.Println("vector result", len(result.Result), ev.expr) 454 for _, ss := range result.Result { 455 fmt.Println(" ", ss.Metric, ss.Value) 456 } 457 return errors.Errorf("expected metric %s with %v not found", ev.metrics[fp], expVals) 458 } 459 } 460 461 case *prometheus.ScalarResult: 462 v, err := parseNumber(fmt.Sprint(result.Result[1])) 463 if err != nil { 464 return err 465 } 466 if len(ev.expected) == 0 || len(ev.expected[0].vals) == 0 { 467 return errors.Errorf("expected no Scalar value but got %v", v) 468 } 469 expected := ev.expected[0].vals[0].Value 470 if !almostEqual(expected, v) { 471 return errors.Errorf("expected Scalar %v but got %v", expected, v) 472 } 473 474 default: 475 panic(errors.Errorf("promql.Test.compareResult: unexpected result type %T", result)) 476 } 477 478 return nil 479 } 480 481 // clearCmd is a command that wipes the test's storage state. 482 type clearCmd struct{} 483 484 func (cmd clearCmd) String() string { 485 return "clear" 486 } 487 488 // Run executes the command sequence of the test. Until the maximum error number 489 // is reached, evaluation errors do not terminate execution. 490 func (t *Test) Run() error { 491 for _, cmd := range t.cmds { 492 // TODO(fabxc): aggregate command errors, yield diffs for result 493 // comparison errors. 494 if err := t.exec(cmd); err != nil { 495 return err 496 } 497 } 498 return nil 499 } 500 501 // exec processes a single step of the test. 502 func (t *Test) exec(tc testCommand) error { 503 switch cmd := tc.(type) { 504 case *clearCmd: 505 return t.clear() 506 507 case *loadCmd: 508 return cmd.append() 509 510 case *evalCmd: 511 expr, err := parser.ParseExpr(cmd.expr) 512 if err != nil { 513 return err 514 } 515 516 t := time.Unix(0, startingTime+(cmd.start.Unix()*1000000000)) 517 bodyBytes, err := cmd.m3query.query(expr.String(), t) 518 if err != nil { 519 if cmd.fail { 520 return nil 521 } 522 return errors.Wrapf(err, "error in %s %s, line %d", cmd, cmd.expr, cmd.line) 523 } 524 if cmd.fail { 525 return fmt.Errorf("expected to fail at %s %s, line %d", cmd, cmd.expr, cmd.line) 526 } 527 528 err = cmd.compareResult(bodyBytes) 529 if err != nil { 530 return errors.Wrapf(err, "error in %s %s, line %d. m3query response: %s", cmd, cmd.expr, cmd.line, string(bodyBytes)) 531 } 532 533 default: 534 panic("promql.Test.exec: unknown test command type") 535 } 536 return nil 537 } 538 539 // clear the current test storage of all inserted samples. 540 func (t *Test) clear() error { 541 return t.m3comparator.clear() 542 } 543 544 // Close closes resources associated with the Test. 545 func (t *Test) Close() { 546 } 547 548 // almostEqual returns true if the two sample lines only differ by a 549 // small relative error in their sample value. 550 func almostEqual(a, b float64) bool { 551 // NaN has no equality but for testing we still want to know whether both values 552 // are NaN. 553 if math.IsNaN(a) && math.IsNaN(b) { 554 return true 555 } 556 557 // Cf. http://floating-point-gui.de/errors/comparison/ 558 if a == b { 559 return true 560 } 561 562 diff := math.Abs(a - b) 563 564 if a == 0 || b == 0 || diff < minNormal { 565 return diff < epsilon*minNormal 566 } 567 return diff/(math.Abs(a)+math.Abs(b)) < epsilon 568 } 569 570 func parseNumber(s string) (float64, error) { 571 n, err := strconv.ParseInt(s, 0, 64) 572 f := float64(n) 573 if err != nil { 574 f, err = strconv.ParseFloat(s, 64) 575 } 576 if err != nil { 577 return 0, errors.Wrap(err, "error parsing number") 578 } 579 return f, nil 580 }