Lazily load samples for unit testing (#4851)

* Lazily load samples for unit testing

Signed-off-by: Ganesh Vernekar <cs15btech11018@iith.ac.in>

* cleanup

Signed-off-by: Ganesh Vernekar <cs15btech11018@iith.ac.in>
This commit is contained in:
Ganesh Vernekar 2018-11-22 14:21:38 +05:30 committed by Goutham Veeramachaneni
parent b98a5acb57
commit cfb3769274
2 changed files with 153 additions and 14 deletions

View file

@ -135,17 +135,12 @@ type testGroup struct {
// test performs the unit tests. // test performs the unit tests.
func (tg *testGroup) test(mint, maxt time.Time, evalInterval time.Duration, groupOrderMap map[string]int, ruleFiles ...string) []error { func (tg *testGroup) test(mint, maxt time.Time, evalInterval time.Duration, groupOrderMap map[string]int, ruleFiles ...string) []error {
// Setup testing suite. // Setup testing suite.
suite, err := promql.NewTest(nil, tg.seriesLoadingString()) suite, err := promql.NewLazyLoader(nil, tg.seriesLoadingString())
if err != nil { if err != nil {
return []error{err} return []error{err}
} }
defer suite.Close() defer suite.Close()
err = suite.Run()
if err != nil {
return []error{err}
}
// Load the rule files. // Load the rule files.
opts := &rules.ManagerOptions{ opts := &rules.ManagerOptions{
QueryFunc: rules.EngineQueryFunc(suite.QueryEngine(), suite.Storage()), QueryFunc: rules.EngineQueryFunc(suite.QueryEngine(), suite.Storage()),
@ -191,8 +186,17 @@ func (tg *testGroup) test(mint, maxt time.Time, evalInterval time.Duration, grou
var errs []error var errs []error
for ts := mint; ts.Before(maxt); ts = ts.Add(evalInterval) { for ts := mint; ts.Before(maxt); ts = ts.Add(evalInterval) {
// Collects the alerts asked for unit testing. // Collects the alerts asked for unit testing.
for _, g := range groups { suite.WithSamplesTill(ts, func(err error) {
g.Eval(suite.Context(), ts) if err != nil {
errs = append(errs, err)
return
}
for _, g := range groups {
g.Eval(suite.Context(), ts)
}
})
if len(errs) > 0 {
return errs
} }
for { for {

View file

@ -15,6 +15,7 @@ package promql
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"math" "math"
@ -105,7 +106,7 @@ func raise(line int, format string, v ...interface{}) error {
} }
} }
func (t *Test) parseLoad(lines []string, i int) (int, *loadCmd, error) { func parseLoad(lines []string, i int) (int, *loadCmd, error) {
if !patLoad.MatchString(lines[i]) { if !patLoad.MatchString(lines[i]) {
return i, nil, raise(i, "invalid load command. (load <step:duration>)") return i, nil, raise(i, "invalid load command. (load <step:duration>)")
} }
@ -196,9 +197,8 @@ func (t *Test) parseEval(lines []string, i int) (int, *evalCmd, error) {
return i, cmd, nil return i, cmd, nil
} }
// parse the given command sequence and appends it to the test. // getLines returns trimmed lines after removing the comments.
func (t *Test) parse(input string) error { func getLines(input string) []string {
// Trim lines and remove comments.
lines := strings.Split(input, "\n") lines := strings.Split(input, "\n")
for i, l := range lines { for i, l := range lines {
l = strings.TrimSpace(l) l = strings.TrimSpace(l)
@ -207,8 +207,13 @@ func (t *Test) parse(input string) error {
} }
lines[i] = l lines[i] = l
} }
var err error return lines
}
// parse the given command sequence and appends it to the test.
func (t *Test) parse(input string) error {
lines := getLines(input)
var err error
// Scan for steps line by line. // Scan for steps line by line.
for i := 0; i < len(lines); i++ { for i := 0; i < len(lines); i++ {
l := lines[i] l := lines[i]
@ -221,7 +226,7 @@ func (t *Test) parse(input string) error {
case c == "clear": case c == "clear":
cmd = &clearCmd{} cmd = &clearCmd{}
case c == "load": case c == "load":
i, cmd, err = t.parseLoad(lines, i) i, cmd, err = parseLoad(lines, i)
case strings.HasPrefix(c, "eval"): case strings.HasPrefix(c, "eval"):
i, cmd, err = t.parseEval(lines, i) i, cmd, err = t.parseEval(lines, i)
default: default:
@ -560,3 +565,133 @@ func parseNumber(s string) (float64, error) {
} }
return f, nil return f, nil
} }
// LazyLoader lazily loads samples into storage.
// This is specifically implemented for unit testing of rules.
type LazyLoader struct {
testutil.T
loadCmd *loadCmd
storage storage.Storage
queryEngine *Engine
context context.Context
cancelCtx context.CancelFunc
}
// NewLazyLoader returns an initialized empty LazyLoader.
func NewLazyLoader(t testutil.T, input string) (*LazyLoader, error) {
ll := &LazyLoader{
T: t,
}
err := ll.parse(input)
ll.clear()
return ll, err
}
// parse the given load command.
func (ll *LazyLoader) parse(input string) error {
lines := getLines(input)
// Accepts only 'load' command.
for i := 0; i < len(lines); i++ {
l := lines[i]
if len(l) == 0 {
continue
}
if strings.ToLower(patSpace.Split(l, 2)[0]) == "load" {
_, cmd, err := parseLoad(lines, i)
if err != nil {
return err
}
ll.loadCmd = cmd
return nil
} else {
return raise(i, "invalid command %q", l)
}
}
return errors.New("no \"load\" command found")
}
// clear the current test storage of all inserted samples.
func (ll *LazyLoader) clear() {
if ll.storage != nil {
if err := ll.storage.Close(); err != nil {
ll.T.Fatalf("closing test storage: %s", err)
}
}
if ll.cancelCtx != nil {
ll.cancelCtx()
}
ll.storage = testutil.NewStorage(ll)
opts := EngineOpts{
Logger: nil,
Reg: nil,
MaxConcurrent: 20,
MaxSamples: 10000,
Timeout: 100 * time.Second,
}
ll.queryEngine = NewEngine(opts)
ll.context, ll.cancelCtx = context.WithCancel(context.Background())
}
// appendTill appends the defined time series to the storage till the given timestamp (in milliseconds).
func (ll *LazyLoader) appendTill(ts int64) error {
app, err := ll.storage.Appender()
if err != nil {
return err
}
for h, smpls := range ll.loadCmd.defs {
m := ll.loadCmd.metrics[h]
for i, s := range smpls {
if s.T > ts {
// Removing the already added samples.
ll.loadCmd.defs[h] = smpls[i:]
break
}
if _, err := app.Add(m, s.T, s.V); err != nil {
return err
}
}
}
return app.Commit()
}
// WithSamplesTill loads the samples till given timestamp and executes the given function.
func (ll *LazyLoader) WithSamplesTill(ts time.Time, fn func(error)) {
tsMilli := ts.Sub(time.Unix(0, 0)) / time.Millisecond
fn(ll.appendTill(int64(tsMilli)))
}
// QueryEngine returns the LazyLoader's query engine.
func (ll *LazyLoader) QueryEngine() *Engine {
return ll.queryEngine
}
// Queryable allows querying the LazyLoader's data.
// Note: only the samples till the max timestamp used
// in `WithSamplesTill` can be queried.
func (ll *LazyLoader) Queryable() storage.Queryable {
return ll.storage
}
// Context returns the LazyLoader's context.
func (ll *LazyLoader) Context() context.Context {
return ll.context
}
// Storage returns the LazyLoader's storage.
func (ll *LazyLoader) Storage() storage.Storage {
return ll.storage
}
// Close closes resources associated with the LazyLoader.
func (ll *LazyLoader) Close() {
ll.cancelCtx()
if err := ll.storage.Close(); err != nil {
ll.T.Fatalf("closing test storage: %s", err)
}
}