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  }