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) }) }