mirror of
https://github.com/prometheus/prometheus.git
synced 2024-12-25 13:44:05 -08:00
[tests]: extend test scripting language to support range queries (#13825)
* Extract method to make it easier to test. Signed-off-by: Charles Korn <charles.korn@grafana.com> * Remove superfluous interface definition. Signed-off-by: Charles Korn <charles.korn@grafana.com> * Add test cases for existing instant query functionality. Signed-off-by: Charles Korn <charles.korn@grafana.com> * Add support for testing range queries Signed-off-by: Charles Korn <charles.korn@grafana.com> * Expand test coverage for instant queries and clarify error when a float is returned but a histogram is expected (or vice versa) Signed-off-by: Charles Korn <charles.korn@grafana.com> * Improve error message formatting Signed-off-by: Charles Korn <charles.korn@grafana.com> * Add test case for instant query command with invalid timestamp Signed-off-by: Charles Korn <charles.korn@grafana.com> * Fix linting warning. Signed-off-by: Charles Korn <charles.korn@grafana.com> * Remove superfluous print statement and expected result Signed-off-by: Charles Korn <charles.korn@grafana.com> * Fix linting warning. Signed-off-by: Charles Korn <charles.korn@grafana.com> * Add note about ordered range eval commands. Signed-off-by: Charles Korn <charles.korn@grafana.com> * Check that matrix results are always sorted by labels. Signed-off-by: Charles Korn <charles.korn@grafana.com> --------- Signed-off-by: Charles Korn <charles.korn@grafana.com>
This commit is contained in:
parent
481f14e1c0
commit
5cc97a1820
431
promql/test.go
431
promql/test.go
|
@ -46,6 +46,7 @@ var (
|
|||
patSpace = regexp.MustCompile("[\t ]+")
|
||||
patLoad = regexp.MustCompile(`^load\s+(.+?)$`)
|
||||
patEvalInstant = regexp.MustCompile(`^eval(?:_(fail|ordered))?\s+instant\s+(?:at\s+(.+?))?\s+(.+)$`)
|
||||
patEvalRange = regexp.MustCompile(`^eval(?:_(fail))?\s+range\s+from\s+(.+)\s+to\s+(.+)\s+step\s+(.+?)\s+(.+)$`)
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -72,7 +73,7 @@ func LoadedStorage(t testutil.T, input string) *teststorage.TestStorage {
|
|||
}
|
||||
|
||||
// RunBuiltinTests runs an acceptance test suite against the provided engine.
|
||||
func RunBuiltinTests(t *testing.T, engine engineQuerier) {
|
||||
func RunBuiltinTests(t *testing.T, engine QueryEngine) {
|
||||
t.Cleanup(func() { parser.EnableExperimentalFunctions = false })
|
||||
parser.EnableExperimentalFunctions = true
|
||||
|
||||
|
@ -89,11 +90,19 @@ func RunBuiltinTests(t *testing.T, engine engineQuerier) {
|
|||
}
|
||||
|
||||
// RunTest parses and runs the test against the provided engine.
|
||||
func RunTest(t testutil.T, input string, engine engineQuerier) {
|
||||
test, err := newTest(t, input)
|
||||
require.NoError(t, err)
|
||||
func RunTest(t testutil.T, input string, engine QueryEngine) {
|
||||
require.NoError(t, runTest(t, input, engine))
|
||||
}
|
||||
|
||||
func runTest(t testutil.T, input string, engine QueryEngine) error {
|
||||
test, err := newTest(t, input)
|
||||
|
||||
// Why do this before checking err? newTest() can create the test storage and then return an error,
|
||||
// and we want to make sure to clean that up to avoid leaking goroutines.
|
||||
defer func() {
|
||||
if test == nil {
|
||||
return
|
||||
}
|
||||
if test.storage != nil {
|
||||
test.storage.Close()
|
||||
}
|
||||
|
@ -102,11 +111,19 @@ func RunTest(t testutil.T, input string, engine engineQuerier) {
|
|||
}
|
||||
}()
|
||||
|
||||
for _, cmd := range test.cmds {
|
||||
// TODO(fabxc): aggregate command errors, yield diffs for result
|
||||
// comparison errors.
|
||||
require.NoError(t, test.exec(cmd, engine))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cmd := range test.cmds {
|
||||
if err := test.exec(cmd, engine); err != nil {
|
||||
// TODO(fabxc): aggregate command errors, yield diffs for result
|
||||
// comparison errors.
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// test is a sequence of read and write commands that are run
|
||||
|
@ -137,11 +154,6 @@ func newTest(t testutil.T, input string) (*test, error) {
|
|||
//go:embed testdata
|
||||
var testsFs embed.FS
|
||||
|
||||
type engineQuerier interface {
|
||||
NewRangeQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, start, end time.Time, interval time.Duration) (Query, error)
|
||||
NewInstantQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, ts time.Time) (Query, error)
|
||||
}
|
||||
|
||||
func raise(line int, format string, v ...interface{}) error {
|
||||
return &parser.ParseErr{
|
||||
LineOffset: line,
|
||||
|
@ -188,15 +200,26 @@ func parseSeries(defLine string, line int) (labels.Labels, []parser.SequenceValu
|
|||
}
|
||||
|
||||
func (t *test) parseEval(lines []string, i int) (int, *evalCmd, error) {
|
||||
if !patEvalInstant.MatchString(lines[i]) {
|
||||
return i, nil, raise(i, "invalid evaluation command. (eval[_fail|_ordered] instant [at <offset:duration>] <query>")
|
||||
instantParts := patEvalInstant.FindStringSubmatch(lines[i])
|
||||
rangeParts := patEvalRange.FindStringSubmatch(lines[i])
|
||||
|
||||
if instantParts == nil && rangeParts == nil {
|
||||
return i, nil, raise(i, "invalid evaluation command. Must be either 'eval[_fail|_ordered] instant [at <offset:duration>] <query>' or 'eval[_fail] range from <from> to <to> step <step> <query>'")
|
||||
}
|
||||
parts := patEvalInstant.FindStringSubmatch(lines[i])
|
||||
var (
|
||||
mod = parts[1]
|
||||
at = parts[2]
|
||||
expr = parts[3]
|
||||
)
|
||||
|
||||
isInstant := instantParts != nil
|
||||
|
||||
var mod string
|
||||
var expr string
|
||||
|
||||
if isInstant {
|
||||
mod = instantParts[1]
|
||||
expr = instantParts[3]
|
||||
} else {
|
||||
mod = rangeParts[1]
|
||||
expr = rangeParts[5]
|
||||
}
|
||||
|
||||
_, err := parser.ParseExpr(expr)
|
||||
if err != nil {
|
||||
parser.EnrichParseError(err, func(parseErr *parser.ParseErr) {
|
||||
|
@ -209,15 +232,54 @@ func (t *test) parseEval(lines []string, i int) (int, *evalCmd, error) {
|
|||
return i, nil, err
|
||||
}
|
||||
|
||||
offset, err := model.ParseDuration(at)
|
||||
if err != nil {
|
||||
return i, nil, raise(i, "invalid step definition %q: %s", parts[1], err)
|
||||
}
|
||||
ts := testStartTime.Add(time.Duration(offset))
|
||||
formatErr := func(format string, args ...any) error {
|
||||
combinedArgs := []any{expr, i + 1}
|
||||
|
||||
combinedArgs = append(combinedArgs, args...)
|
||||
return fmt.Errorf("error in eval %s (line %v): "+format, combinedArgs...)
|
||||
}
|
||||
|
||||
var cmd *evalCmd
|
||||
|
||||
if isInstant {
|
||||
at := instantParts[2]
|
||||
offset, err := model.ParseDuration(at)
|
||||
if err != nil {
|
||||
return i, nil, formatErr("invalid timestamp definition %q: %s", at, err)
|
||||
}
|
||||
ts := testStartTime.Add(time.Duration(offset))
|
||||
cmd = newInstantEvalCmd(expr, ts, i+1)
|
||||
} else {
|
||||
from := rangeParts[2]
|
||||
to := rangeParts[3]
|
||||
step := rangeParts[4]
|
||||
|
||||
parsedFrom, err := model.ParseDuration(from)
|
||||
if err != nil {
|
||||
return i, nil, formatErr("invalid start timestamp definition %q: %s", from, err)
|
||||
}
|
||||
|
||||
parsedTo, err := model.ParseDuration(to)
|
||||
if err != nil {
|
||||
return i, nil, formatErr("invalid end timestamp definition %q: %s", to, err)
|
||||
}
|
||||
|
||||
if parsedTo < parsedFrom {
|
||||
return i, nil, formatErr("invalid test definition, end timestamp (%s) is before start timestamp (%s)", to, from)
|
||||
}
|
||||
|
||||
parsedStep, err := model.ParseDuration(step)
|
||||
if err != nil {
|
||||
return i, nil, formatErr("invalid step definition %q: %s", step, err)
|
||||
}
|
||||
|
||||
cmd = newRangeEvalCmd(expr, testStartTime.Add(time.Duration(parsedFrom)), testStartTime.Add(time.Duration(parsedTo)), time.Duration(parsedStep), i+1)
|
||||
}
|
||||
|
||||
cmd := newEvalCmd(expr, ts, i+1)
|
||||
switch mod {
|
||||
case "ordered":
|
||||
// Ordered results are not supported for range queries, but the regex for range query commands does not allow
|
||||
// asserting an ordered result, so we don't need to do any error checking here.
|
||||
cmd.ordered = true
|
||||
case "fail":
|
||||
cmd.fail = true
|
||||
|
@ -240,8 +302,8 @@ func (t *test) parseEval(lines []string, i int) (int, *evalCmd, error) {
|
|||
}
|
||||
|
||||
// Currently, we are not expecting any matrices.
|
||||
if len(vals) > 1 {
|
||||
return i, nil, raise(i, "expecting multiple values in instant evaluation not allowed")
|
||||
if len(vals) > 1 && isInstant {
|
||||
return i, nil, formatErr("expecting multiple values in instant evaluation not allowed")
|
||||
}
|
||||
cmd.expectMetric(j, metric, vals...)
|
||||
}
|
||||
|
@ -375,8 +437,11 @@ func appendSample(a storage.Appender, s Sample, m labels.Labels) error {
|
|||
type evalCmd struct {
|
||||
expr string
|
||||
start time.Time
|
||||
end time.Time
|
||||
step time.Duration
|
||||
line int
|
||||
|
||||
isRange bool // if false, instant query
|
||||
fail, ordered bool
|
||||
|
||||
metrics map[uint64]labels.Labels
|
||||
|
@ -392,7 +457,7 @@ func (e entry) String() string {
|
|||
return fmt.Sprintf("%d: %s", e.pos, e.vals)
|
||||
}
|
||||
|
||||
func newEvalCmd(expr string, start time.Time, line int) *evalCmd {
|
||||
func newInstantEvalCmd(expr string, start time.Time, line int) *evalCmd {
|
||||
return &evalCmd{
|
||||
expr: expr,
|
||||
start: start,
|
||||
|
@ -403,6 +468,20 @@ func newEvalCmd(expr string, start time.Time, line int) *evalCmd {
|
|||
}
|
||||
}
|
||||
|
||||
func newRangeEvalCmd(expr string, start, end time.Time, step time.Duration, line int) *evalCmd {
|
||||
return &evalCmd{
|
||||
expr: expr,
|
||||
start: start,
|
||||
end: end,
|
||||
step: step,
|
||||
line: line,
|
||||
isRange: true,
|
||||
|
||||
metrics: map[uint64]labels.Labels{},
|
||||
expected: map[uint64]entry{},
|
||||
}
|
||||
}
|
||||
|
||||
func (ev *evalCmd) String() string {
|
||||
return "eval"
|
||||
}
|
||||
|
@ -425,7 +504,77 @@ func (ev *evalCmd) expectMetric(pos int, m labels.Labels, vals ...parser.Sequenc
|
|||
func (ev *evalCmd) compareResult(result parser.Value) error {
|
||||
switch val := result.(type) {
|
||||
case Matrix:
|
||||
return errors.New("received range result on instant evaluation")
|
||||
if ev.ordered {
|
||||
return fmt.Errorf("expected ordered result, but query returned a matrix")
|
||||
}
|
||||
|
||||
if err := assertMatrixSorted(val); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
seen := map[uint64]bool{}
|
||||
for _, s := range val {
|
||||
hash := s.Metric.Hash()
|
||||
if _, ok := ev.metrics[hash]; !ok {
|
||||
return fmt.Errorf("unexpected metric %s in result", s.Metric)
|
||||
}
|
||||
seen[hash] = true
|
||||
exp := ev.expected[hash]
|
||||
|
||||
var expectedFloats []FPoint
|
||||
var expectedHistograms []HPoint
|
||||
|
||||
for i, e := range exp.vals {
|
||||
ts := ev.start.Add(time.Duration(i) * ev.step)
|
||||
|
||||
if ts.After(ev.end) {
|
||||
return fmt.Errorf("expected %v points for %s, but query time range cannot return this many points", len(exp.vals), ev.metrics[hash])
|
||||
}
|
||||
|
||||
t := ts.UnixNano() / int64(time.Millisecond/time.Nanosecond)
|
||||
|
||||
if e.Histogram != nil {
|
||||
expectedHistograms = append(expectedHistograms, HPoint{T: t, H: e.Histogram})
|
||||
} else if !e.Omitted {
|
||||
expectedFloats = append(expectedFloats, FPoint{T: t, F: e.Value})
|
||||
}
|
||||
}
|
||||
|
||||
if len(expectedFloats) != len(s.Floats) || len(expectedHistograms) != len(s.Histograms) {
|
||||
return fmt.Errorf("expected %v float points and %v histogram points for %s, but got %s", len(expectedFloats), len(expectedHistograms), ev.metrics[hash], formatSeriesResult(s))
|
||||
}
|
||||
|
||||
for i, expected := range expectedFloats {
|
||||
actual := s.Floats[i]
|
||||
|
||||
if expected.T != actual.T {
|
||||
return fmt.Errorf("expected float value at index %v for %s to have timestamp %v, but it had timestamp %v (result has %s)", i, ev.metrics[hash], expected.T, actual.T, formatSeriesResult(s))
|
||||
}
|
||||
|
||||
if !almostEqual(actual.F, expected.F, defaultEpsilon) {
|
||||
return fmt.Errorf("expected float value at index %v (t=%v) for %s to be %v, but got %v (result has %s)", i, actual.T, ev.metrics[hash], expected.F, actual.F, formatSeriesResult(s))
|
||||
}
|
||||
}
|
||||
|
||||
for i, expected := range expectedHistograms {
|
||||
actual := s.Histograms[i]
|
||||
|
||||
if expected.T != actual.T {
|
||||
return fmt.Errorf("expected histogram value at index %v for %s to have timestamp %v, but it had timestamp %v (result has %s)", i, ev.metrics[hash], expected.T, actual.T, formatSeriesResult(s))
|
||||
}
|
||||
|
||||
if !actual.H.Equals(expected.H) {
|
||||
return fmt.Errorf("expected histogram value at index %v (t=%v) for %s to be %v, but got %v (result has %s)", i, actual.T, ev.metrics[hash], expected.H, actual.H, formatSeriesResult(s))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for hash := range ev.expected {
|
||||
if !seen[hash] {
|
||||
return fmt.Errorf("expected metric %s not found", ev.metrics[hash])
|
||||
}
|
||||
}
|
||||
|
||||
case Vector:
|
||||
seen := map[uint64]bool{}
|
||||
|
@ -440,7 +589,13 @@ func (ev *evalCmd) compareResult(result parser.Value) error {
|
|||
}
|
||||
exp0 := exp.vals[0]
|
||||
expH := exp0.Histogram
|
||||
if (expH == nil) != (v.H == nil) || (expH != nil && !expH.Equals(v.H)) {
|
||||
if expH == nil && v.H != nil {
|
||||
return fmt.Errorf("expected float value %v for %s but got histogram %s", exp0, v.Metric, HistogramTestExpression(v.H))
|
||||
}
|
||||
if expH != nil && v.H == nil {
|
||||
return fmt.Errorf("expected histogram %s for %s but got float value %v", HistogramTestExpression(expH), v.Metric, v.F)
|
||||
}
|
||||
if expH != nil && !expH.Equals(v.H) {
|
||||
return fmt.Errorf("expected %v for %s but got %s", HistogramTestExpression(expH), v.Metric, HistogramTestExpression(v.H))
|
||||
}
|
||||
if !almostEqual(exp0.Value, v.F, defaultEpsilon) {
|
||||
|
@ -477,6 +632,21 @@ func (ev *evalCmd) compareResult(result parser.Value) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func formatSeriesResult(s Series) string {
|
||||
floatPlural := "s"
|
||||
histogramPlural := "s"
|
||||
|
||||
if len(s.Floats) == 1 {
|
||||
floatPlural = ""
|
||||
}
|
||||
|
||||
if len(s.Histograms) == 1 {
|
||||
histogramPlural = ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v float point%s %v and %v histogram point%s %v", len(s.Floats), floatPlural, s.Floats, len(s.Histograms), histogramPlural, s.Histograms)
|
||||
}
|
||||
|
||||
// HistogramTestExpression returns TestExpression() for the given histogram or "" if the histogram is nil.
|
||||
func HistogramTestExpression(h *histogram.FloatHistogram) string {
|
||||
if h != nil {
|
||||
|
@ -561,7 +731,7 @@ func atModifierTestCases(exprStr string, evalTime time.Time) ([]atModifierTestCa
|
|||
}
|
||||
|
||||
// exec processes a single step of the test.
|
||||
func (t *test) exec(tc testCommand, engine engineQuerier) error {
|
||||
func (t *test) exec(tc testCommand, engine QueryEngine) error {
|
||||
switch cmd := tc.(type) {
|
||||
case *clearCmd:
|
||||
t.clear()
|
||||
|
@ -578,74 +748,7 @@ func (t *test) exec(tc testCommand, engine engineQuerier) error {
|
|||
}
|
||||
|
||||
case *evalCmd:
|
||||
queries, err := atModifierTestCases(cmd.expr, cmd.start)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queries = append([]atModifierTestCase{{expr: cmd.expr, evalTime: cmd.start}}, queries...)
|
||||
for _, iq := range queries {
|
||||
q, err := engine.NewInstantQuery(t.context, t.storage, nil, iq.expr, iq.evalTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer q.Close()
|
||||
res := q.Exec(t.context)
|
||||
if res.Err != nil {
|
||||
if cmd.fail {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("error evaluating query %q (line %d): %w", iq.expr, cmd.line, res.Err)
|
||||
}
|
||||
if res.Err == nil && cmd.fail {
|
||||
return fmt.Errorf("expected error evaluating query %q (line %d) but got none", iq.expr, cmd.line)
|
||||
}
|
||||
err = cmd.compareResult(res.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in %s %s (line %d): %w", cmd, iq.expr, cmd.line, err)
|
||||
}
|
||||
|
||||
// Check query returns same result in range mode,
|
||||
// by checking against the middle step.
|
||||
q, err = engine.NewRangeQuery(t.context, t.storage, nil, iq.expr, iq.evalTime.Add(-time.Minute), iq.evalTime.Add(time.Minute), time.Minute)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rangeRes := q.Exec(t.context)
|
||||
if rangeRes.Err != nil {
|
||||
return fmt.Errorf("error evaluating query %q (line %d) in range mode: %w", iq.expr, cmd.line, rangeRes.Err)
|
||||
}
|
||||
defer q.Close()
|
||||
if cmd.ordered {
|
||||
// Ordering isn't defined for range queries.
|
||||
continue
|
||||
}
|
||||
mat := rangeRes.Value.(Matrix)
|
||||
vec := make(Vector, 0, len(mat))
|
||||
for _, series := range mat {
|
||||
// We expect either Floats or Histograms.
|
||||
for _, point := range series.Floats {
|
||||
if point.T == timeMilliseconds(iq.evalTime) {
|
||||
vec = append(vec, Sample{Metric: series.Metric, T: point.T, F: point.F})
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, point := range series.Histograms {
|
||||
if point.T == timeMilliseconds(iq.evalTime) {
|
||||
vec = append(vec, Sample{Metric: series.Metric, T: point.T, H: point.H})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, ok := res.Value.(Scalar); ok {
|
||||
err = cmd.compareResult(Scalar{V: vec[0].F})
|
||||
} else {
|
||||
err = cmd.compareResult(vec)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in %s %s (line %d) range mode: %w", cmd, iq.expr, cmd.line, err)
|
||||
}
|
||||
|
||||
}
|
||||
return t.execEval(cmd, engine)
|
||||
|
||||
default:
|
||||
panic("promql.Test.exec: unknown test command type")
|
||||
|
@ -653,6 +756,132 @@ func (t *test) exec(tc testCommand, engine engineQuerier) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (t *test) execEval(cmd *evalCmd, engine QueryEngine) error {
|
||||
if cmd.isRange {
|
||||
return t.execRangeEval(cmd, engine)
|
||||
}
|
||||
|
||||
return t.execInstantEval(cmd, engine)
|
||||
}
|
||||
|
||||
func (t *test) execRangeEval(cmd *evalCmd, engine QueryEngine) error {
|
||||
q, err := engine.NewRangeQuery(t.context, t.storage, nil, cmd.expr, cmd.start, cmd.end, cmd.step)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res := q.Exec(t.context)
|
||||
if res.Err != nil {
|
||||
if cmd.fail {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("error evaluating query %q (line %d): %w", cmd.expr, cmd.line, res.Err)
|
||||
}
|
||||
if res.Err == nil && cmd.fail {
|
||||
return fmt.Errorf("expected error evaluating query %q (line %d) but got none", cmd.expr, cmd.line)
|
||||
}
|
||||
defer q.Close()
|
||||
|
||||
if err := cmd.compareResult(res.Value); err != nil {
|
||||
return fmt.Errorf("error in %s %s (line %d): %w", cmd, cmd.expr, cmd.line, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *test) execInstantEval(cmd *evalCmd, engine QueryEngine) error {
|
||||
queries, err := atModifierTestCases(cmd.expr, cmd.start)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queries = append([]atModifierTestCase{{expr: cmd.expr, evalTime: cmd.start}}, queries...)
|
||||
for _, iq := range queries {
|
||||
q, err := engine.NewInstantQuery(t.context, t.storage, nil, iq.expr, iq.evalTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer q.Close()
|
||||
res := q.Exec(t.context)
|
||||
if res.Err != nil {
|
||||
if cmd.fail {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("error evaluating query %q (line %d): %w", iq.expr, cmd.line, res.Err)
|
||||
}
|
||||
if res.Err == nil && cmd.fail {
|
||||
return fmt.Errorf("expected error evaluating query %q (line %d) but got none", iq.expr, cmd.line)
|
||||
}
|
||||
err = cmd.compareResult(res.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in %s %s (line %d): %w", cmd, iq.expr, cmd.line, err)
|
||||
}
|
||||
|
||||
// Check query returns same result in range mode,
|
||||
// by checking against the middle step.
|
||||
q, err = engine.NewRangeQuery(t.context, t.storage, nil, iq.expr, iq.evalTime.Add(-time.Minute), iq.evalTime.Add(time.Minute), time.Minute)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rangeRes := q.Exec(t.context)
|
||||
if rangeRes.Err != nil {
|
||||
return fmt.Errorf("error evaluating query %q (line %d) in range mode: %w", iq.expr, cmd.line, rangeRes.Err)
|
||||
}
|
||||
defer q.Close()
|
||||
if cmd.ordered {
|
||||
// Range queries are always sorted by labels, so skip this test case that expects results in a particular order.
|
||||
continue
|
||||
}
|
||||
mat := rangeRes.Value.(Matrix)
|
||||
if err := assertMatrixSorted(mat); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vec := make(Vector, 0, len(mat))
|
||||
for _, series := range mat {
|
||||
// We expect either Floats or Histograms.
|
||||
for _, point := range series.Floats {
|
||||
if point.T == timeMilliseconds(iq.evalTime) {
|
||||
vec = append(vec, Sample{Metric: series.Metric, T: point.T, F: point.F})
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, point := range series.Histograms {
|
||||
if point.T == timeMilliseconds(iq.evalTime) {
|
||||
vec = append(vec, Sample{Metric: series.Metric, T: point.T, H: point.H})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, ok := res.Value.(Scalar); ok {
|
||||
err = cmd.compareResult(Scalar{V: vec[0].F})
|
||||
} else {
|
||||
err = cmd.compareResult(vec)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in %s %s (line %d) range mode: %w", cmd, iq.expr, cmd.line, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func assertMatrixSorted(m Matrix) error {
|
||||
if len(m) <= 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, s := range m[:len(m)-1] {
|
||||
nextIndex := i + 1
|
||||
nextMetric := m[nextIndex].Metric
|
||||
|
||||
if labels.Compare(s.Metric, nextMetric) > 0 {
|
||||
return fmt.Errorf("matrix results should always be sorted by labels, but matrix is not sorted: series at index %v with labels %s sorts before series at index %v with labels %s", nextIndex, nextMetric, i, s.Metric)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clear the current test storage of all inserted samples.
|
||||
func (t *test) clear() {
|
||||
if t.storage != nil {
|
||||
|
|
|
@ -156,3 +156,354 @@ func TestLazyLoader_WithSamplesTill(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunTest(t *testing.T) {
|
||||
testData := `
|
||||
load 5m
|
||||
http_requests{job="api-server", instance="0", group="production"} 0+10x10
|
||||
http_requests{job="api-server", instance="1", group="production"} 0+20x10
|
||||
http_requests{job="api-server", instance="0", group="canary"} 0+30x10
|
||||
http_requests{job="api-server", instance="1", group="canary"} 0+40x10
|
||||
`
|
||||
|
||||
testCases := map[string]struct {
|
||||
input string
|
||||
expectedError string
|
||||
}{
|
||||
"instant query with expected float result": {
|
||||
input: testData + `
|
||||
eval instant at 5m sum by (group) (http_requests)
|
||||
{group="production"} 30
|
||||
{group="canary"} 70
|
||||
`,
|
||||
},
|
||||
"instant query with unexpected float result": {
|
||||
input: testData + `
|
||||
eval instant at 5m sum by (group) (http_requests)
|
||||
{group="production"} 30
|
||||
{group="canary"} 80
|
||||
`,
|
||||
expectedError: `error in eval sum by (group) (http_requests) (line 8): expected 80 for {group="canary"} but got 70`,
|
||||
},
|
||||
"instant query with expected histogram result": {
|
||||
input: `
|
||||
load 5m
|
||||
testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}}
|
||||
|
||||
eval instant at 0 testmetric
|
||||
testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}}
|
||||
`,
|
||||
},
|
||||
"instant query with unexpected histogram result": {
|
||||
input: `
|
||||
load 5m
|
||||
testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}}
|
||||
|
||||
eval instant at 0 testmetric
|
||||
testmetric {{schema:-1 sum:6 count:1 buckets:[1] offset:1}}
|
||||
`,
|
||||
expectedError: `error in eval testmetric (line 5): expected {{schema:-1 count:1 sum:6 offset:1 buckets:[1]}} for {__name__="testmetric"} but got {{schema:-1 count:1 sum:4 offset:1 buckets:[1]}}`,
|
||||
},
|
||||
"instant query with float value returned when histogram expected": {
|
||||
input: `
|
||||
load 5m
|
||||
testmetric 2
|
||||
|
||||
eval instant at 0 testmetric
|
||||
testmetric {{}}
|
||||
`,
|
||||
expectedError: `error in eval testmetric (line 5): expected histogram {{}} for {__name__="testmetric"} but got float value 2`,
|
||||
},
|
||||
"instant query with histogram returned when float expected": {
|
||||
input: `
|
||||
load 5m
|
||||
testmetric {{}}
|
||||
|
||||
eval instant at 0 testmetric
|
||||
testmetric 2
|
||||
`,
|
||||
expectedError: `error in eval testmetric (line 5): expected float value 2.000000 for {__name__="testmetric"} but got histogram {{}}`,
|
||||
},
|
||||
"instant query, but result has an unexpected series": {
|
||||
input: testData + `
|
||||
eval instant at 5m sum by (group) (http_requests)
|
||||
{group="production"} 30
|
||||
`,
|
||||
expectedError: `error in eval sum by (group) (http_requests) (line 8): unexpected metric {group="canary"} in result`,
|
||||
},
|
||||
"instant query, but result is missing a series": {
|
||||
input: testData + `
|
||||
eval instant at 5m sum by (group) (http_requests)
|
||||
{group="production"} 30
|
||||
{group="canary"} 70
|
||||
{group="test"} 100
|
||||
`,
|
||||
expectedError: `error in eval sum by (group) (http_requests) (line 8): expected metric {group="test"} with 3: [100.000000] not found`,
|
||||
},
|
||||
"instant query expected to fail, and query fails": {
|
||||
input: `
|
||||
load 5m
|
||||
testmetric1{src="a",dst="b"} 0
|
||||
testmetric2{src="a",dst="b"} 1
|
||||
|
||||
eval_fail instant at 0m ceil({__name__=~'testmetric1|testmetric2'})
|
||||
`,
|
||||
},
|
||||
"instant query expected to fail, but query succeeds": {
|
||||
input: `eval_fail instant at 0s vector(0)`,
|
||||
expectedError: `expected error evaluating query "vector(0)" (line 1) but got none`,
|
||||
},
|
||||
"instant query with results expected to match provided order, and result is in expected order": {
|
||||
input: testData + `
|
||||
eval_ordered instant at 50m sort(http_requests)
|
||||
http_requests{group="production", instance="0", job="api-server"} 100
|
||||
http_requests{group="production", instance="1", job="api-server"} 200
|
||||
http_requests{group="canary", instance="0", job="api-server"} 300
|
||||
http_requests{group="canary", instance="1", job="api-server"} 400
|
||||
`,
|
||||
},
|
||||
"instant query with results expected to match provided order, but result is out of order": {
|
||||
input: testData + `
|
||||
eval_ordered instant at 50m sort(http_requests)
|
||||
http_requests{group="production", instance="0", job="api-server"} 100
|
||||
http_requests{group="production", instance="1", job="api-server"} 200
|
||||
http_requests{group="canary", instance="1", job="api-server"} 400
|
||||
http_requests{group="canary", instance="0", job="api-server"} 300
|
||||
`,
|
||||
expectedError: `error in eval sort(http_requests) (line 8): expected metric {__name__="http_requests", group="canary", instance="0", job="api-server"} with [300.000000] at position 4 but was at 3`,
|
||||
},
|
||||
"instant query with results expected to match provided order, but result has an unexpected series": {
|
||||
input: testData + `
|
||||
eval_ordered instant at 50m sort(http_requests)
|
||||
http_requests{group="production", instance="0", job="api-server"} 100
|
||||
http_requests{group="production", instance="1", job="api-server"} 200
|
||||
http_requests{group="canary", instance="0", job="api-server"} 300
|
||||
`,
|
||||
expectedError: `error in eval sort(http_requests) (line 8): unexpected metric {__name__="http_requests", group="canary", instance="1", job="api-server"} in result`,
|
||||
},
|
||||
"instant query with invalid timestamp": {
|
||||
input: `eval instant at abc123 vector(0)`,
|
||||
expectedError: `error in eval vector(0) (line 1): invalid timestamp definition "abc123": not a valid duration string: "abc123"`,
|
||||
},
|
||||
"range query with expected result": {
|
||||
input: testData + `
|
||||
eval range from 0 to 10m step 5m sum by (group) (http_requests)
|
||||
{group="production"} 0 30 60
|
||||
{group="canary"} 0 70 140
|
||||
`,
|
||||
},
|
||||
"range query with unexpected float value": {
|
||||
input: testData + `
|
||||
eval range from 0 to 10m step 5m sum by (group) (http_requests)
|
||||
{group="production"} 0 30 60
|
||||
{group="canary"} 0 80 140
|
||||
`,
|
||||
expectedError: `error in eval sum by (group) (http_requests) (line 8): expected float value at index 1 (t=300000) for {group="canary"} to be 80, but got 70 (result has 3 float points [0 @[0] 70 @[300000] 140 @[600000]] and 0 histogram points [])`,
|
||||
},
|
||||
"range query with expected histogram values": {
|
||||
input: `
|
||||
load 5m
|
||||
testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} {{schema:-1 sum:5 count:1 buckets:[1] offset:1}} {{schema:-1 sum:6 count:1 buckets:[1] offset:1}}
|
||||
|
||||
eval range from 0 to 10m step 5m testmetric
|
||||
testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} {{schema:-1 sum:5 count:1 buckets:[1] offset:1}} {{schema:-1 sum:6 count:1 buckets:[1] offset:1}}
|
||||
`,
|
||||
},
|
||||
"range query with unexpected histogram value": {
|
||||
input: `
|
||||
load 5m
|
||||
testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} {{schema:-1 sum:5 count:1 buckets:[1] offset:1}} {{schema:-1 sum:6 count:1 buckets:[1] offset:1}}
|
||||
|
||||
eval range from 0 to 10m step 5m testmetric
|
||||
testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} {{schema:-1 sum:7 count:1 buckets:[1] offset:1}} {{schema:-1 sum:8 count:1 buckets:[1] offset:1}}
|
||||
`,
|
||||
expectedError: `error in eval testmetric (line 5): expected histogram value at index 1 (t=300000) for {__name__="testmetric"} to be {count:1, sum:7, (1,4]:1}, but got {count:1, sum:5, (1,4]:1} (result has 0 float points [] and 3 histogram points [{count:1, sum:4, (1,4]:1} @[0] {count:1, sum:5, (1,4]:1} @[300000] {count:1, sum:6, (1,4]:1} @[600000]])`,
|
||||
},
|
||||
"range query with too many points for query time range": {
|
||||
input: testData + `
|
||||
eval range from 0 to 10m step 5m sum by (group) (http_requests)
|
||||
{group="production"} 0 30 60 90
|
||||
{group="canary"} 0 70 140
|
||||
`,
|
||||
expectedError: `error in eval sum by (group) (http_requests) (line 8): expected 4 points for {group="production"}, but query time range cannot return this many points`,
|
||||
},
|
||||
"range query with missing point in result": {
|
||||
input: `
|
||||
load 5m
|
||||
testmetric 5
|
||||
|
||||
eval range from 0 to 6m step 6m testmetric
|
||||
testmetric 5 10
|
||||
`,
|
||||
expectedError: `error in eval testmetric (line 5): expected 2 float points and 0 histogram points for {__name__="testmetric"}, but got 1 float point [5 @[0]] and 0 histogram points []`,
|
||||
},
|
||||
"range query with extra point in result": {
|
||||
input: testData + `
|
||||
eval range from 0 to 10m step 5m sum by (group) (http_requests)
|
||||
{group="production"} 0 30
|
||||
{group="canary"} 0 70 140
|
||||
`,
|
||||
expectedError: `error in eval sum by (group) (http_requests) (line 8): expected 2 float points and 0 histogram points for {group="production"}, but got 3 float points [0 @[0] 30 @[300000] 60 @[600000]] and 0 histogram points []`,
|
||||
},
|
||||
"range query, but result has an unexpected series": {
|
||||
input: testData + `
|
||||
eval range from 0 to 10m step 5m sum by (group) (http_requests)
|
||||
{group="production"} 0 30 60
|
||||
`,
|
||||
expectedError: `error in eval sum by (group) (http_requests) (line 8): unexpected metric {group="canary"} in result`,
|
||||
},
|
||||
"range query, but result is missing a series": {
|
||||
input: testData + `
|
||||
eval range from 0 to 10m step 5m sum by (group) (http_requests)
|
||||
{group="production"} 0 30 60
|
||||
{group="canary"} 0 70 140
|
||||
{group="test"} 0 100 200
|
||||
`,
|
||||
expectedError: `error in eval sum by (group) (http_requests) (line 8): expected metric {group="test"} not found`,
|
||||
},
|
||||
"range query expected to fail, and query fails": {
|
||||
input: `
|
||||
load 5m
|
||||
testmetric1{src="a",dst="b"} 0
|
||||
testmetric2{src="a",dst="b"} 1
|
||||
|
||||
eval_fail range from 0 to 10m step 5m ceil({__name__=~'testmetric1|testmetric2'})
|
||||
`,
|
||||
},
|
||||
"range query expected to fail, but query succeeds": {
|
||||
input: `eval_fail range from 0 to 10m step 5m vector(0)`,
|
||||
expectedError: `expected error evaluating query "vector(0)" (line 1) but got none`,
|
||||
},
|
||||
"range query with from and to timestamps in wrong order": {
|
||||
input: `eval range from 10m to 9m step 5m vector(0)`,
|
||||
expectedError: `error in eval vector(0) (line 1): invalid test definition, end timestamp (9m) is before start timestamp (10m)`,
|
||||
},
|
||||
"range query with sparse output": {
|
||||
input: `
|
||||
load 6m
|
||||
testmetric 1 _ 3
|
||||
|
||||
eval range from 0 to 18m step 6m testmetric
|
||||
testmetric 1 _ 3
|
||||
`,
|
||||
},
|
||||
"range query with float value returned when no value expected": {
|
||||
input: `
|
||||
load 6m
|
||||
testmetric 1 2 3
|
||||
|
||||
eval range from 0 to 18m step 6m testmetric
|
||||
testmetric 1 _ 3
|
||||
`,
|
||||
expectedError: `error in eval testmetric (line 5): expected 2 float points and 0 histogram points for {__name__="testmetric"}, but got 3 float points [1 @[0] 2 @[360000] 3 @[720000]] and 0 histogram points []`,
|
||||
},
|
||||
"range query with float value returned when histogram expected": {
|
||||
input: `
|
||||
load 5m
|
||||
testmetric 2 3
|
||||
|
||||
eval range from 0 to 5m step 5m testmetric
|
||||
testmetric {{}} {{}}
|
||||
`,
|
||||
expectedError: `error in eval testmetric (line 5): expected 0 float points and 2 histogram points for {__name__="testmetric"}, but got 2 float points [2 @[0] 3 @[300000]] and 0 histogram points []`,
|
||||
},
|
||||
"range query with histogram returned when float expected": {
|
||||
input: `
|
||||
load 5m
|
||||
testmetric {{}} {{}}
|
||||
|
||||
eval range from 0 to 5m step 5m testmetric
|
||||
testmetric 2 3
|
||||
`,
|
||||
expectedError: `error in eval testmetric (line 5): expected 2 float points and 0 histogram points for {__name__="testmetric"}, but got 0 float points [] and 2 histogram points [{count:0, sum:0} @[0] {count:0, sum:0} @[300000]]`,
|
||||
},
|
||||
"range query with expected mixed results": {
|
||||
input: `
|
||||
load 6m
|
||||
testmetric{group="a"} {{}} _ _
|
||||
testmetric{group="b"} _ _ 3
|
||||
|
||||
eval range from 0 to 12m step 6m sum(testmetric)
|
||||
{} {{}} _ 3
|
||||
`,
|
||||
},
|
||||
"range query with mixed results and incorrect values": {
|
||||
input: `
|
||||
load 5m
|
||||
testmetric 3 {{}}
|
||||
|
||||
eval range from 0 to 5m step 5m testmetric
|
||||
testmetric {{}} 3
|
||||
`,
|
||||
expectedError: `error in eval testmetric (line 5): expected float value at index 0 for {__name__="testmetric"} to have timestamp 300000, but it had timestamp 0 (result has 1 float point [3 @[0]] and 1 histogram point [{count:0, sum:0} @[300000]])`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := runTest(t, testCase.input, newTestEngine())
|
||||
|
||||
if testCase.expectedError == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.EqualError(t, err, testCase.expectedError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssertMatrixSorted(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
matrix Matrix
|
||||
expectedError string
|
||||
}{
|
||||
"empty matrix": {
|
||||
matrix: Matrix{},
|
||||
},
|
||||
"matrix with one series": {
|
||||
matrix: Matrix{
|
||||
Series{Metric: labels.FromStrings("the_label", "value_1")},
|
||||
},
|
||||
},
|
||||
"matrix with two series, series in sorted order": {
|
||||
matrix: Matrix{
|
||||
Series{Metric: labels.FromStrings("the_label", "value_1")},
|
||||
Series{Metric: labels.FromStrings("the_label", "value_2")},
|
||||
},
|
||||
},
|
||||
"matrix with two series, series in reverse order": {
|
||||
matrix: Matrix{
|
||||
Series{Metric: labels.FromStrings("the_label", "value_2")},
|
||||
Series{Metric: labels.FromStrings("the_label", "value_1")},
|
||||
},
|
||||
expectedError: `matrix results should always be sorted by labels, but matrix is not sorted: series at index 1 with labels {the_label="value_1"} sorts before series at index 0 with labels {the_label="value_2"}`,
|
||||
},
|
||||
"matrix with three series, series in sorted order": {
|
||||
matrix: Matrix{
|
||||
Series{Metric: labels.FromStrings("the_label", "value_1")},
|
||||
Series{Metric: labels.FromStrings("the_label", "value_2")},
|
||||
Series{Metric: labels.FromStrings("the_label", "value_3")},
|
||||
},
|
||||
},
|
||||
"matrix with three series, series not in sorted order": {
|
||||
matrix: Matrix{
|
||||
Series{Metric: labels.FromStrings("the_label", "value_1")},
|
||||
Series{Metric: labels.FromStrings("the_label", "value_3")},
|
||||
Series{Metric: labels.FromStrings("the_label", "value_2")},
|
||||
},
|
||||
expectedError: `matrix results should always be sorted by labels, but matrix is not sorted: series at index 2 with labels {the_label="value_2"} sorts before series at index 1 with labels {the_label="value_3"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := assertMatrixSorted(testCase.matrix)
|
||||
|
||||
if testCase.expectedError == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.EqualError(t, err, testCase.expectedError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue