diff --git a/core/engine_test.go b/core/engine_test.go
index 7e148ba5d6a..b4868d24d00 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 bb3d58c4ebc..59481571be5 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,185 @@ 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 protect 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 newline*
+// 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
+// newline -> linefeed | crlf
+// crlf -> carriage_return linefeed
+// linefeed -> "\n"
+// carriage_return -> "\r"
+// 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 {
+ return nil, fmt.Errorf("parsing threshold condition %s failed"+
+ "; reason: unable to cast parsed expression to []interface{}"+
+ "it looks like you've found a bug, we'd be grateful if you would consider "+
+ "opening an issue in the K6 repository (https://github.com/grafana/k6/issues/new)",
+ expression,
+ )
+ } else if len(parsed) != 3 {
+ return nil, fmt.Errorf("parsing threshold condition %s failed"+
+ "; reason: parsed %d expression tokens, expected 3 (aggregation_method operator value, as in rate<100)"+
+ "it looks like you've found a bug, we'd be grateful if you would consider "+
+ "opening an issue in the K6 repository (https://github.com/grafana/k6/issues/new)",
+ expression,
+ len(parsed),
+ )
+ }
+
+ // 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" +
+ "it looks like you've found a bug, we'd be grateful if you would consider " +
+ "opening an issue in the K6 repository (https://github.com/grafana/k6/issues/new)",
+ )
+ }
+ operator, ok := parsed[1].(string)
+ if !ok {
+ return nil, fmt.Errorf("the threshold expression parser failed; " +
+ "reason: unable to cast parsed operator to string" +
+ "it looks like you've found a bug, we'd be grateful if you would consider " +
+ "opening an issue in the K6 repository (https://github.com/grafana/k6/issues/new)",
+ )
+ }
+
+ 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)" +
+ "it looks like you've found a bug, we'd be grateful if you would consider " +
+ "opening an issue in the K6 repository (https://github.com/grafana/k6/issues/new)",
+ )
+ }
+
+ return &thresholdCondition{AggregationMethod: method, Operator: operator, Value: value}, nil
}
type thresholdConfig struct {
@@ -98,11 +212,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 +236,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 +252,54 @@ 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
+ ts.sinked = make(map[string]float64)
+ 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 4d06dd0f05f..51f00f13566 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 protect 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)
})
}