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  }