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  }