From 4f61eb424c1339e1a35efca22c2bf3637b7adfe6 Mon Sep 17 00:00:00 2001 From: oleiade Date: Wed, 17 Nov 2021 09:07:33 +0100 Subject: [PATCH] Remove the JS runtime from threshold calculations In this commit we replace the previously existing thresholds condition evaluation, which was depending on Goja's Runtime, with a new pure-Go one. Thresholds are now parsed, and evaluated in Go, and no JS rutimes are involved in the process anymore. It is built upong the thresholds parser, and parser combinators library introduced in previous commits. --- core/engine_test.go | 29 +-- stats/thresholds.go | 222 ++++++++++++++------- stats/thresholds_test.go | 417 +++++++++++++++++++++++++++++---------- 3 files changed, 486 insertions(+), 182 deletions(-) diff --git a/core/engine_test.go b/core/engine_test.go index 7e148ba5d6a3..b4868d24d009 100644 --- a/core/engine_test.go +++ b/core/engine_test.go @@ -258,7 +258,7 @@ func TestEngine_processSamples(t *testing.T) { }) t.Run("submetric", func(t *testing.T) { t.Parallel() - ths, err := stats.NewThresholds([]string{`1+1==2`}) + ths, err := stats.NewThresholds([]string{`value<2`}) assert.NoError(t, err) e, _, wait := newTestEngine(t, nil, nil, nil, lib.Options{ @@ -286,7 +286,10 @@ func TestEngineThresholdsWillAbort(t *testing.T) { t.Parallel() metric := stats.New("my_metric", stats.Gauge) - ths, err := stats.NewThresholds([]string{"1+1==3"}) + // The incoming samples for the metric set it to 1.25. Considering + // the metric is of type Gauge, value > 1.25 should always fail, and + // trigger an abort. + ths, err := stats.NewThresholds([]string{"value>1.25"}) assert.NoError(t, err) ths.Thresholds[0].AbortOnFail = true @@ -305,7 +308,11 @@ func TestEngineAbortedByThresholds(t *testing.T) { t.Parallel() metric := stats.New("my_metric", stats.Gauge) - ths, err := stats.NewThresholds([]string{"1+1==3"}) + // The MiniRunner sets the value of the metric to 1.25. Considering + // the metric is of type Gauge, value > 1.25 should always fail, and + // trigger an abort. + // **N.B**: a threshold returning an error, won't trigger an abort. + ths, err := stats.NewThresholds([]string{"value>1.25"}) assert.NoError(t, err) ths.Thresholds[0].AbortOnFail = true @@ -343,14 +350,14 @@ func TestEngine_processThresholds(t *testing.T) { ths map[string][]string abort bool }{ - "passing": {true, map[string][]string{"my_metric": {"1+1==2"}}, false}, - "failing": {false, map[string][]string{"my_metric": {"1+1==3"}}, false}, - "aborting": {false, map[string][]string{"my_metric": {"1+1==3"}}, true}, - - "submetric,match,passing": {true, map[string][]string{"my_metric{a:1}": {"1+1==2"}}, false}, - "submetric,match,failing": {false, map[string][]string{"my_metric{a:1}": {"1+1==3"}}, false}, - "submetric,nomatch,passing": {true, map[string][]string{"my_metric{a:2}": {"1+1==2"}}, false}, - "submetric,nomatch,failing": {true, map[string][]string{"my_metric{a:2}": {"1+1==3"}}, false}, + "passing": {true, map[string][]string{"my_metric": {"value<2"}}, false}, + "failing": {false, map[string][]string{"my_metric": {"value>1.25"}}, false}, + "aborting": {false, map[string][]string{"my_metric": {"value>1.25"}}, true}, + + "submetric,match,passing": {true, map[string][]string{"my_metric{a:1}": {"value<2"}}, false}, + "submetric,match,failing": {false, map[string][]string{"my_metric{a:1}": {"value>1.25"}}, false}, + "submetric,nomatch,passing": {true, map[string][]string{"my_metric{a:2}": {"value<2"}}, false}, + "submetric,nomatch,failing": {true, map[string][]string{"my_metric{a:2}": {"value>1.25"}}, false}, } for name, data := range testdata { diff --git a/stats/thresholds.go b/stats/thresholds.go index bb3d58c4ebc8..085cf0e7b005 100644 --- a/stats/thresholds.go +++ b/stats/thresholds.go @@ -17,7 +17,6 @@ * along with this program. If not, see . * */ - package stats import ( @@ -26,70 +25,162 @@ import ( "fmt" "time" - "github.com/dop251/goja" - "go.k6.io/k6/lib/types" ) -const jsEnvSrc = ` -function p(pct) { - return __sink__.P(pct/100.0); -}; -` - -var jsEnv *goja.Program - -func init() { - pgm, err := goja.Compile("__env__", jsEnvSrc, true) - if err != nil { - panic(err) - } - jsEnv = pgm -} - // Threshold is a representation of a single threshold for a single metric type Threshold struct { // Source is the text based source of the threshold Source string - // LastFailed is a makrer if the last testing of this threshold failed + // LastFailed is a marker if the last testing of this threshold failed LastFailed bool // AbortOnFail marks if a given threshold fails that the whole test should be aborted AbortOnFail bool // AbortGracePeriod is a the minimum amount of time a test should be running before a failing // this threshold will abort the test AbortGracePeriod types.NullDuration - - pgm *goja.Program - rt *goja.Runtime + // parsed is the threshold condition parsed from the Source expression + parsed *thresholdCondition } -func newThreshold(src string, newThreshold *goja.Runtime, abortOnFail bool, gracePeriod types.NullDuration) (*Threshold, error) { - pgm, err := goja.Compile("__threshold__", src, true) +func newThreshold(src string, abortOnFail bool, gracePeriod types.NullDuration) (*Threshold, error) { + condition, err := parseThresholdCondition(src) if err != nil { return nil, err } return &Threshold{ Source: src, + parsed: condition, AbortOnFail: abortOnFail, AbortGracePeriod: gracePeriod, - pgm: pgm, - rt: newThreshold, }, nil } -func (t Threshold) runNoTaint() (bool, error) { - v, err := t.rt.RunProgram(t.pgm) - if err != nil { - return false, err +func (t *Threshold) runNoTaint(sinks map[string]float64) (bool, error) { + // Extract the sink value for the aggregation method used in the threshold + // expression + lhs, ok := sinks[t.parsed.AggregationMethod] + if !ok { + return false, fmt.Errorf("unable to apply threshold %s over metrics; reason: "+ + "no metric supporting the %s aggregation method found", + t.parsed.AggregationMethod, + t.parsed.AggregationMethod) + } + + // Apply the threshold expression operator to the left and + // right hand side values + var passes bool + switch t.parsed.Operator { + case ">": + passes = lhs > t.parsed.Value + case ">=": + passes = lhs >= t.parsed.Value + case "<=": + passes = lhs <= t.parsed.Value + case "<": + passes = lhs < t.parsed.Value + case "==": + passes = lhs == t.parsed.Value + case "===": + // Considering a sink always maps to float64 values, + // strictly equal is equivalent to loosely equal + passes = lhs == t.parsed.Value + case "!=": + passes = lhs != t.parsed.Value + default: + // The ParseThresholdCondition constructor should ensure that no invalid + // operator gets through, but let's protech our future selves anyhow. + return false, fmt.Errorf("unable to apply threshold %s over metrics; "+ + "reason: %s is an invalid operator", + t.Source, + t.parsed.Operator, + ) } - return v.ToBoolean(), nil + + // Perform the actual threshold verification + return passes, nil } -func (t *Threshold) run() (bool, error) { - b, err := t.runNoTaint() - t.LastFailed = !b - return b, err +func (t *Threshold) run(sinks map[string]float64) (bool, error) { + passes, err := t.runNoTaint(sinks) + t.LastFailed = !passes + return passes, err +} + +type thresholdCondition struct { + AggregationMethod string + Operator string + Value float64 +} + +// ParseThresholdCondition parses a threshold condition expression, +// as defined in a JS script (for instance p(95)<1000), into a ThresholdCondition +// instance, using our parser combinators package. + +// This parser expect a threshold expression matching the following BNF +// +// ``` +// assertion -> aggregation_method whitespace* operator whitespace* float +// aggregation_method -> trend | rate | gauge | counter +// counter -> "count" | "sum" | "rate" +// gauge -> "last" | "min" | "max" | "value" +// rate -> "rate" +// trend -> "min" | "mean" | "avg" | "max" | percentile +// percentile -> "p(" float ")" +// operator -> ">" | ">=" | "<=" | "<" | "==" | "===" | "!=" +// float -> digit+ (. digit+)? +// digit -> "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" +// whitespace -> space | tab +// tab -> "\t" +// space -> " " +// ``` +func parseThresholdCondition(expression string) (*thresholdCondition, error) { + parser := ParseAssertion() + + // Parse the Threshold as provided in the JS script options thresholds value (p(95)<1000) + result := parser([]rune(expression)) + if result.Err != nil { + return nil, fmt.Errorf("parsing threshold condition %s failed; "+ + "reason: the parser failed on %s", + expression, + result.Err.ErrorAtChar([]rune(expression))) + } + + // The Sequence combinator will return a slice of interface{} + // instances. Up to us to decide what we want to cast them down + // to. + // Considering our expression format, the parser should return a slice + // of size 3 to us: aggregation_method operator sink_value. The type system + // ensures us it should be the case too, but let's protect our future selves anyhow. + var ok bool + parsed, ok := result.Payload.([]interface{}) + if !ok || len(parsed) != 3 { + return nil, fmt.Errorf("parsing threshold condition %s failed; reason: malformed expression", expression) + } + + // Unpack the various components of the parsed threshold expression + method, ok := parsed[0].(string) + if !ok { + return nil, fmt.Errorf("the threshold expression parser failed; " + + "reason: unable to cast parsed aggregation method to string", + ) + } + operator, ok := parsed[1].(string) + if !ok { + return nil, fmt.Errorf("the threshold expression parser failed; " + + "reason: unable to cast parsed operator to string", + ) + } + + value, ok := parsed[2].(float64) + if !ok { + return nil, fmt.Errorf("the threshold expression parser failed; " + + "reason: unable to cast parsed value to underlying type (float64)", + ) + } + + return &thresholdCondition{AggregationMethod: method, Operator: operator, Value: value}, nil } type thresholdConfig struct { @@ -98,11 +189,11 @@ type thresholdConfig struct { AbortGracePeriod types.NullDuration `json:"delayAbortEval"` } -//used internally for JSON marshalling +// used internally for JSON marshalling type rawThresholdConfig thresholdConfig func (tc *thresholdConfig) UnmarshalJSON(data []byte) error { - //shortcircuit unmarshalling for simple string format + // shortcircuit unmarshalling for simple string format if err := json.Unmarshal(data, &tc.Threshold); err == nil { return nil } @@ -122,9 +213,9 @@ func (tc thresholdConfig) MarshalJSON() ([]byte, error) { // Thresholds is the combination of all Thresholds for a given metric type Thresholds struct { - Runtime *goja.Runtime Thresholds []*Threshold Abort bool + Sinked map[string]float64 } // NewThresholds returns Thresholds objects representing the provided source strings @@ -138,60 +229,53 @@ func NewThresholds(sources []string) (Thresholds, error) { } func newThresholdsWithConfig(configs []thresholdConfig) (Thresholds, error) { - rt := goja.New() - if _, err := rt.RunProgram(jsEnv); err != nil { - return Thresholds{}, fmt.Errorf("threshold builtin error: %w", err) - } + thresholds := make([]*Threshold, len(configs)) + sinked := make(map[string]float64) - ts := make([]*Threshold, len(configs)) for i, config := range configs { - t, err := newThreshold(config.Threshold, rt, config.AbortOnFail, config.AbortGracePeriod) + t, err := newThreshold(config.Threshold, config.AbortOnFail, config.AbortGracePeriod) if err != nil { return Thresholds{}, fmt.Errorf("threshold %d error: %w", i, err) } - ts[i] = t + thresholds[i] = t } - return Thresholds{rt, ts, false}, nil + return Thresholds{thresholds, false, sinked}, nil } -func (ts *Thresholds) updateVM(sink Sink, t time.Duration) error { - ts.Runtime.Set("__sink__", sink) - f := sink.Format(t) - for k, v := range f { - ts.Runtime.Set(k, v) - } - return nil -} - -func (ts *Thresholds) runAll(t time.Duration) (bool, error) { - succ := true - for i, th := range ts.Thresholds { - b, err := th.run() +func (ts *Thresholds) runAll(duration time.Duration) (bool, error) { + succeeded := true + for i, threshold := range ts.Thresholds { + b, err := threshold.run(ts.Sinked) if err != nil { return false, fmt.Errorf("threshold %d run error: %w", i, err) } + if !b { - succ = false + succeeded = false - if ts.Abort || !th.AbortOnFail { + if ts.Abort || !threshold.AbortOnFail { continue } - ts.Abort = !th.AbortGracePeriod.Valid || - th.AbortGracePeriod.Duration < types.Duration(t) + ts.Abort = !threshold.AbortGracePeriod.Valid || + threshold.AbortGracePeriod.Duration < types.Duration(duration) } } - return succ, nil + + return succeeded, nil } // Run processes all the thresholds with the provided Sink at the provided time and returns if any // of them fails -func (ts *Thresholds) Run(sink Sink, t time.Duration) (bool, error) { - if err := ts.updateVM(sink, t); err != nil { - return false, err +func (ts *Thresholds) Run(sink Sink, duration time.Duration) (bool, error) { + // Update the sinks store + f := sink.Format(duration) + for k, v := range f { + ts.Sinked[k] = v } - return ts.runAll(t) + + return ts.runAll(duration) } // UnmarshalJSON is implementation of json.Unmarshaler diff --git a/stats/thresholds_test.go b/stats/thresholds_test.go index 4d06dd0f05f8..51ab12009e5f 100644 --- a/stats/thresholds_test.go +++ b/stats/thresholds_test.go @@ -22,79 +22,257 @@ package stats import ( "encoding/json" + "reflect" "testing" "time" - "github.com/dop251/goja" "github.com/stretchr/testify/assert" - "go.k6.io/k6/lib/types" ) func TestNewThreshold(t *testing.T) { - src := `1+1==2` - rt := goja.New() + t.Parallel() + + // Arrange + src := `rate<0.01` abortOnFail := false gracePeriod := types.NullDurationFrom(2 * time.Second) - th, err := newThreshold(src, rt, abortOnFail, gracePeriod) + + // Act + threshold, err := newThreshold(src, abortOnFail, gracePeriod) + + // Assert assert.NoError(t, err) + assert.Equal(t, src, threshold.Source) + assert.False(t, threshold.LastFailed) + assert.Equal(t, abortOnFail, threshold.AbortOnFail) + assert.Equal(t, gracePeriod, threshold.AbortGracePeriod) +} + +func TestNewThreshold_InvalidThresholdConditionExpression(t *testing.T) { + t.Parallel() + + // Arrange + src := "1+1==2" + abortOnFail := false + gracePeriod := types.NullDurationFrom(2 * time.Second) - assert.Equal(t, src, th.Source) - assert.False(t, th.LastFailed) - assert.NotNil(t, th.pgm) - assert.Equal(t, rt, th.rt) - assert.Equal(t, abortOnFail, th.AbortOnFail) - assert.Equal(t, gracePeriod, th.AbortGracePeriod) + // Act + th, err := newThreshold(src, abortOnFail, gracePeriod) + + // Assert + assert.Error(t, err, "instantiating a threshold with an invalid expression should fail") + assert.Nil(t, th, "instantiating a threshold with an invalid expression should return a nil Threshold") +} + +func TestThreshold_runNoTaint(t *testing.T) { + t.Parallel() + + type fields struct { + Source string + LastFailed bool + AbortOnFail bool + AbortGracePeriod types.NullDuration + parsed *thresholdCondition + } + type args struct { + sinks map[string]float64 + } + tests := []struct { + name string + fields fields + args args + want bool + wantErr bool + }{ + { + "valid expression over passing threshold", + fields{"rate<0.01", false, false, types.NullDurationFrom(2 * time.Second), &thresholdCondition{"rate", "<", 0.01}}, + args{map[string]float64{"rate": 0.00001}}, + true, + false, + }, + { + "valid expression over failing threshold", + fields{"rate>0.01", false, false, types.NullDurationFrom(2 * time.Second), &thresholdCondition{"rate", ">", 0.01}}, + args{map[string]float64{"rate": 0.00001}}, + false, + false, + }, + { + "valid expression over non-existing sink", + fields{"rate>0.01", false, false, types.NullDurationFrom(2 * time.Second), &thresholdCondition{"rate", ">", 0.01}}, + args{map[string]float64{"med": 27.2}}, + false, + true, + }, + { + // The ParseThresholdCondition constructor should ensure that no invalid + // operator gets through, but let's protech our future selves anyhow. + "invalid expression operator", + fields{"rate&0.01", false, false, types.NullDurationFrom(2 * time.Second), &thresholdCondition{"rate", "&", 0.01}}, + args{map[string]float64{"rate": 0.00001}}, + false, + true, + }, + } + for _, testCase := range tests { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + threshold := &Threshold{ + Source: testCase.fields.Source, + LastFailed: testCase.fields.LastFailed, + AbortOnFail: testCase.fields.AbortOnFail, + AbortGracePeriod: testCase.fields.AbortGracePeriod, + parsed: testCase.fields.parsed, + } + got, err := threshold.runNoTaint(testCase.args.sinks) + if (err != nil) != testCase.wantErr { + t.Errorf("Threshold.runNoTaint() error = %v, wantErr %v", err, testCase.wantErr) + return + } + if got != testCase.want { + t.Errorf("Threshold.runNoTaint() = %v, want %v", got, testCase.want) + } + }) + } } func TestThresholdRun(t *testing.T) { + t.Parallel() + t.Run("true", func(t *testing.T) { - th, err := newThreshold(`1+1==2`, goja.New(), false, types.NullDuration{}) + t.Parallel() + + sinks := map[string]float64{"rate": 0.0001} + threshold, err := newThreshold(`rate<0.01`, false, types.NullDuration{}) assert.NoError(t, err) t.Run("no taint", func(t *testing.T) { - b, err := th.runNoTaint() + b, err := threshold.runNoTaint(sinks) assert.NoError(t, err) assert.True(t, b) - assert.False(t, th.LastFailed) + assert.False(t, threshold.LastFailed) }) t.Run("taint", func(t *testing.T) { - b, err := th.run() + t.Parallel() + + b, err := threshold.run(sinks) assert.NoError(t, err) assert.True(t, b) - assert.False(t, th.LastFailed) + assert.False(t, threshold.LastFailed) }) }) t.Run("false", func(t *testing.T) { - th, err := newThreshold(`1+1==4`, goja.New(), false, types.NullDuration{}) + t.Parallel() + + sinks := map[string]float64{"rate": 1} + threshold, err := newThreshold(`rate<0.01`, false, types.NullDuration{}) assert.NoError(t, err) t.Run("no taint", func(t *testing.T) { - b, err := th.runNoTaint() + b, err := threshold.runNoTaint(sinks) assert.NoError(t, err) assert.False(t, b) - assert.False(t, th.LastFailed) + assert.False(t, threshold.LastFailed) }) t.Run("taint", func(t *testing.T) { - b, err := th.run() + b, err := threshold.run(sinks) assert.NoError(t, err) assert.False(t, b) - assert.True(t, th.LastFailed) + assert.True(t, threshold.LastFailed) }) }) } +func TestParseThresholdCondition(t *testing.T) { + t.Parallel() + + type args struct { + expression string + } + tests := []struct { + name string + args args + want *thresholdCondition + wantErr bool + }{ + {"valid Counter count expression with Integer value", args{"count<100"}, &thresholdCondition{"count", "<", 100}, false}, + {"valid Counter count expression with Real value", args{"count<100.10"}, &thresholdCondition{"count", "<", 100.10}, false}, + {"valid Counter rate expression with Integer value", args{"rate<100"}, &thresholdCondition{"rate", "<", 100}, false}, + {"valid Counter rate expression with Real value", args{"rate<100.10"}, &thresholdCondition{"rate", "<", 100.10}, false}, + {"valid Gauge value expression with Integer value", args{"value<100"}, &thresholdCondition{"value", "<", 100}, false}, + {"valid Gauge value expression with Real value", args{"value<100.10"}, &thresholdCondition{"value", "<", 100.10}, false}, + {"valid Rate rate expression with Integer value", args{"rate<100"}, &thresholdCondition{"rate", "<", 100}, false}, + {"valid Rate rate expression with Real value", args{"rate<100.10"}, &thresholdCondition{"rate", "<", 100.10}, false}, + {"valid Trend avg expression with Integer value", args{"avg<100"}, &thresholdCondition{"avg", "<", 100}, false}, + {"valid Trend avg expression with Real value", args{"avg<100.10"}, &thresholdCondition{"avg", "<", 100.10}, false}, + {"valid Trend min expression with Integer value", args{"avg<100"}, &thresholdCondition{"avg", "<", 100}, false}, + {"valid Trend min expression with Real value", args{"min<100.10"}, &thresholdCondition{"min", "<", 100.10}, false}, + {"valid Trend max expression with Integer value", args{"max<100"}, &thresholdCondition{"max", "<", 100}, false}, + {"valid Trend max expression with Real value", args{"max<100.10"}, &thresholdCondition{"max", "<", 100.10}, false}, + {"valid Trend med expression with Integer value", args{"med<100"}, &thresholdCondition{"med", "<", 100}, false}, + {"valid Trend med expression with Real value", args{"med<100.10"}, &thresholdCondition{"med", "<", 100.10}, false}, + {"valid Trend percentile expression with Integer N and Integer value", args{"p(99)<100"}, &thresholdCondition{"p(99)", "<", 100}, false}, + {"valid Trend percentile expression with Integer N and Real value", args{"p(99)<100.10"}, &thresholdCondition{"p(99)", "<", 100.10}, false}, + {"valid Trend percentile expression with Real N and Integer value", args{"p(99.9)<100"}, &thresholdCondition{"p(99.9)", "<", 100}, false}, + {"valid Trend percentile expression with Real N and Real value", args{"p(99.9)<100.10"}, &thresholdCondition{"p(99.9)", "<", 100.10}, false}, + {"valid Trend percentile expression with Real N and Real value", args{"p(99.9)<100.10"}, &thresholdCondition{"p(99.9)", "<", 100.10}, false}, + {"valid > operator", args{"med>100"}, &thresholdCondition{"med", ">", 100}, false}, + {"valid > operator", args{"med>=100"}, &thresholdCondition{"med", ">=", 100}, false}, + {"valid > operator", args{"med<100"}, &thresholdCondition{"med", "<", 100}, false}, + {"valid > operator", args{"med<=100"}, &thresholdCondition{"med", "<=", 100}, false}, + {"valid > operator", args{"med==100"}, &thresholdCondition{"med", "==", 100}, false}, + {"valid > operator", args{"med===100"}, &thresholdCondition{"med", "===", 100}, false}, + {"valid > operator", args{"med!=100"}, &thresholdCondition{"med", "!=", 100}, false}, + {"threshold expressions whitespaces are ignored", args{"count \t<\t\t\t 200 "}, &thresholdCondition{"count", "<", 200}, false}, + {"threshold expressions newlines are ignored", args{"count<200\n"}, &thresholdCondition{"count", "<", 200}, false}, + {"non-existing aggregation method", args{"foo<100"}, nil, true}, + {"malformed aggregation method", args{"mad<100"}, nil, true}, + {"non-existing operator", args{"med&100"}, nil, true}, + {"malformed operator", args{"med&=100"}, nil, true}, + {"no value", args{"med<"}, nil, true}, + {"invalid type value (boolean)", args{"med0"}) - assert.NoError(t, err) - - t.Run("error", func(t *testing.T) { - b, err := ts.Run(DummySink{}, 0) - assert.Error(t, err) - assert.False(t, b) - }) +func TestThresholds_Run(t *testing.T) { + t.Parallel() - t.Run("pass", func(t *testing.T) { - b, err := ts.Run(DummySink{"a": 1234.5}, 0) - assert.NoError(t, err) - assert.True(t, b) - }) - - t.Run("fail", func(t *testing.T) { - b, err := ts.Run(DummySink{"a": 0}, 0) - assert.NoError(t, err) - assert.False(t, b) - }) + type args struct { + sink Sink + duration time.Duration + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + "Running thresholds of existing sink", + args{DummySink{"p(95)": 1234.5}, 0}, + true, + false, + }, + { + "Running thresholds of existing sink but failing threshold", + args{DummySink{"p(95)": 3000}, 0}, + false, + false, + }, + { + "Running threshold on non existing sink fails", + args{DummySink{"dummy": 0}, 0}, + false, + true, + }, + } + for _, testCase := range tests { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + thresholds, err := NewThresholds([]string{"p(95)<2000"}) + assert.NoError(t, err, "Initializing new thresholds should not fail") + + got, err := thresholds.Run(testCase.args.sink, testCase.args.duration) + if (err != nil) != testCase.wantErr { + t.Errorf("Thresholds.Run() error = %v, wantErr %v", err, testCase.wantErr) + return + } + if got != testCase.want { + t.Errorf("Thresholds.Run() = %v, want %v", got, testCase.want) + } + }) + } } func TestThresholdsJSON(t *testing.T) { - var testdata = []struct { + t.Parallel() + + testdata := []struct { JSON string - srcs []string + sources []string abortOnFail bool gracePeriod types.NullDuration outputJSON string @@ -234,8 +441,8 @@ func TestThresholdsJSON(t *testing.T) { "", }, { - `["1+1==2"]`, - []string{"1+1==2"}, + `["rate<0.01"]`, + []string{"rate<0.01"}, false, types.NullDuration{}, "", @@ -248,55 +455,59 @@ func TestThresholdsJSON(t *testing.T) { `["rate<0.01"]`, }, { - `["1+1==2","1+1==3"]`, - []string{"1+1==2", "1+1==3"}, + `["rate<0.01","p(95)<200"]`, + []string{"rate<0.01", "p(95)<200"}, false, types.NullDuration{}, "", }, { - `[{"threshold":"1+1==2"}]`, - []string{"1+1==2"}, + `[{"threshold":"rate<0.01"}]`, + []string{"rate<0.01"}, false, types.NullDuration{}, - `["1+1==2"]`, + `["rate<0.01"]`, }, { - `[{"threshold":"1+1==2","abortOnFail":true,"delayAbortEval":null}]`, - []string{"1+1==2"}, + `[{"threshold":"rate<0.01","abortOnFail":true,"delayAbortEval":null}]`, + []string{"rate<0.01"}, true, types.NullDuration{}, "", }, { - `[{"threshold":"1+1==2","abortOnFail":true,"delayAbortEval":"2s"}]`, - []string{"1+1==2"}, + `[{"threshold":"rate<0.01","abortOnFail":true,"delayAbortEval":"2s"}]`, + []string{"rate<0.01"}, true, types.NullDurationFrom(2 * time.Second), "", }, { - `[{"threshold":"1+1==2","abortOnFail":false}]`, - []string{"1+1==2"}, + `[{"threshold":"rate<0.01","abortOnFail":false}]`, + []string{"rate<0.01"}, false, types.NullDuration{}, - `["1+1==2"]`, + `["rate<0.01"]`, }, { - `[{"threshold":"1+1==2"}, "1+1==3"]`, - []string{"1+1==2", "1+1==3"}, + `[{"threshold":"rate<0.01"}, "p(95)<200"]`, + []string{"rate<0.01", "p(95)<200"}, false, types.NullDuration{}, - `["1+1==2","1+1==3"]`, + `["rate<0.01","p(95)<200"]`, }, } for _, data := range testdata { + data := data + t.Run(data.JSON, func(t *testing.T) { + t.Parallel() + var ts Thresholds assert.NoError(t, json.Unmarshal([]byte(data.JSON), &ts)) - assert.Equal(t, len(data.srcs), len(ts.Thresholds)) - for i, src := range data.srcs { + assert.Equal(t, len(data.sources), len(ts.Thresholds)) + for i, src := range data.sources { assert.Equal(t, src, ts.Thresholds[i].Source) assert.Equal(t, data.abortOnFail, ts.Thresholds[i].AbortOnFail) assert.Equal(t, data.gracePeriod, ts.Thresholds[i].AbortGracePeriod) @@ -315,18 +526,20 @@ func TestThresholdsJSON(t *testing.T) { } t.Run("bad JSON", func(t *testing.T) { + t.Parallel() + var ts Thresholds assert.Error(t, json.Unmarshal([]byte("42"), &ts)) assert.Nil(t, ts.Thresholds) - assert.Nil(t, ts.Runtime) assert.False(t, ts.Abort) }) t.Run("bad source", func(t *testing.T) { + t.Parallel() + var ts Thresholds assert.Error(t, json.Unmarshal([]byte(`["="]`), &ts)) assert.Nil(t, ts.Thresholds) - assert.Nil(t, ts.Runtime) assert.False(t, ts.Abort) }) }