blob: 8bce8f54700d0bbcc716eacc939b7b8ed7b0dfd7 [file] [log] [blame]
package rules
import (
"bytes"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/BurntSushi/toml"
"github.com/jmoiron/sqlx"
assert "github.com/stretchr/testify/require"
"go.skia.org/infra/alertserver/go/alerting"
"go.skia.org/infra/go/database/testutil"
"go.skia.org/infra/go/influxdb"
"go.skia.org/infra/go/testutils"
)
type mockClient struct {
Result []*influxdb.Point
}
func (c mockClient) Query(database, query string, n int) ([]*influxdb.Point, error) {
return c.Result, nil
}
func getRule() *Rule {
r := &Rule{
Name: "TestRule",
Database: "DefaultDatabase",
Query: "DummyQuery",
Category: "testing",
Message: "Dummy query meets dummy conditions!",
Conditions: []string{"x > 0", "y > 10 * x", "x < 4"},
client: &mockClient{
Result: []*influxdb.Point{
&influxdb.Point{
Tags: map[string]string{
"tagKey": "tagValue",
},
Values: []json.Number{"1.0", "15.0"},
},
},
},
AutoDismiss: int64(time.Second),
Actions: []string{"Print"},
}
return r
}
// clearDB initializes the database, upgrading it if needed, and removes all
// data to ensure that the test begins with a clean slate. Returns a MySQLTestDatabase
// which must be closed after the test finishes.
func clearDB(t *testing.T) *testutil.MySQLTestDatabase {
failMsg := "Database initialization failed. Do you have the test database set up properly? Details: %v"
// Set up the database.
testDb := testutil.SetupMySQLTestDatabase(t, alerting.MigrationSteps())
conf := testutil.LocalTestDatabaseConfig(alerting.MigrationSteps())
DB, err := sqlx.Open("mysql", conf.MySQLString())
assert.NoError(t, err, failMsg)
alerting.DB = DB
return testDb
}
func TestRuleTriggeringE2E(t *testing.T) {
testutils.MediumTest(t)
testutils.SkipIfShort(t)
d := clearDB(t)
defer d.Close(t)
am, err := alerting.MakeAlertManager(50*time.Millisecond, nil)
assert.NoError(t, err)
getAlerts := func() []*alerting.Alert {
b := bytes.NewBuffer([]byte{})
assert.NoError(t, am.WriteActiveAlertsJson(b, func(*alerting.Alert) bool { return true }))
var active []*alerting.Alert
assert.NoError(t, json.Unmarshal(b.Bytes(), &active))
return active
}
getAlert := func() *alerting.Alert {
active := getAlerts()
assert.Equal(t, 1, len(active))
return active[0]
}
assert.Equal(t, 0, len(getAlerts()))
// Ensure that the rule triggers an alert.
r := getRule()
assert.NoError(t, r.tick(am))
getAlert()
// Ensure that the rule auto-dismisses.
// Hack the conditions so that it's no longer true with the fake query results.
r.Conditions = []string{"x > 2", "y > 10 * x", "x < 4"}
assert.NoError(t, r.tick(am))
time.Sleep(2 * time.Second)
assert.Equal(t, 0, len(getAlerts()))
// Stop the AlertManager.
am.Stop()
}
type mockAlerter struct {
Alerts []*alerting.Alert
}
func (m *mockAlerter) AddAlert(a *alerting.Alert) error {
m.Alerts = append(m.Alerts, a)
return nil
}
func TestEmptyResultsError(t *testing.T) {
testutils.SmallTest(t)
am := &mockAlerter{}
// Ensure that the rule triggers an alert when results are empty.
r := &Rule{
Name: "TestRule",
Database: "DefaultDatabase",
Query: "DummyQuery",
Category: "testing",
Message: "Dummy query meets dummy conditions!",
Conditions: []string{"x > 0", "y > 10 * x", "x < 4"},
client: &mockClient{
Result: []*influxdb.Point{},
},
AutoDismiss: int64(time.Second),
Actions: []string{"Print"},
}
assert.NoError(t, r.tick(am))
assert.Equal(t, 1, len(am.Alerts))
assert.Equal(t, "Failed to execute query for rule \"TestRule\": [ DummyQuery ] The logs might provide more information: http://104.154.112.114:10115/alertserver.ERROR?page_y=end", am.Alerts[0].Message)
}
func TestEmptyResultsOk(t *testing.T) {
testutils.SmallTest(t)
am := &mockAlerter{}
// Ensure that the rule does not trigger an alert when EmptyResultsOk is
// true.
r := &Rule{
Name: "TestRule",
Database: "DefaultDatabase",
Query: "DummyQuery",
EmptyResultsOk: true,
Category: "testing",
Message: "Dummy query meets dummy conditions!",
Conditions: []string{"x > 0", "y > 10 * x", "x < 4"},
client: &mockClient{
Result: []*influxdb.Point{},
},
AutoDismiss: int64(time.Second),
Actions: []string{"Print"},
}
assert.NoError(t, r.tick(am))
assert.Equal(t, 0, len(am.Alerts))
}
func TestRuleParsing(t *testing.T) {
testutils.SmallTest(t)
type parseCase struct {
Name string
Input string
ExpectedErr error
ExpectedErr1_6 error
}
cases := []parseCase{
parseCase{
Name: "Good",
Input: `[[rule]]
name = "randombits"
message = "randombits generates more 1's than 0's in last 5 seconds"
database = "graphite"
query = "select mean(value) from random_bits where time > now() - 5s"
category = "testing"
conditions = ["x > 0.5"]
actions = ["Print"]
auto-dismiss = false
`,
ExpectedErr: nil,
},
parseCase{
Name: "GoodTwoVariables",
Input: `[[rule]]
name = "randombits"
message = "randombits generates more 1's than 0's in last 5 seconds"
database = "graphite"
query = "select mean(value), sum(value) from random_bits where time > now() - 5s"
category = "testing"
conditions = ["x > 0.5", "y < 0.5"]
actions = ["Print"]
auto-dismiss = false
`,
ExpectedErr: nil,
},
parseCase{
Name: "GoodOneVariableTwice",
Input: `[[rule]]
name = "randombits"
message = "randombits generates more 1's than 0's in last 5 seconds"
database = "graphite"
query = "select mean(value) from random_bits where time > now() - 5s"
category = "testing"
conditions = ["x > 0.5", "x < 1.0"]
actions = ["Print"]
auto-dismiss = false
nag = "1h10m"
`,
ExpectedErr: nil,
},
parseCase{
Name: "GoodTwoVariables, One condition",
Input: `[[rule]]
name = "randombits"
message = "randombits generates more 1's than 0's in last 5 seconds"
database = "graphite"
query = "select max(value), min(value) from random_bits where time > now() - 5s"
category = "testing"
conditions = ["x > y"]
actions = ["Print"]
auto-dismiss = false
`,
ExpectedErr: nil,
},
parseCase{
Name: "GoodEmptyResultsOk",
Input: `[[rule]]
name = "randombits"
message = "randombits generates more 1's than 0's in last 5 seconds"
database = "graphite"
query = "select mean(value) from random_bits where time > now() - 5s"
category = "testing"
conditions = ["x > 0.5"]
actions = ["Print"]
auto-dismiss = false
empty-results-ok = true
`,
ExpectedErr: nil,
},
parseCase{
Name: "NoName",
Input: `[[rule]]
message = "randombits generates more 1's than 0's in last 5 seconds"
database = "graphite"
query = "select mean(value) from random_bits where time > now() - 5s"
category = "testing"
conditions = ["x > 0.5"]
actions = ["Print"]
auto-dismiss = false
`,
ExpectedErr: fmt.Errorf("Alert rule missing field \"name\""),
},
parseCase{
Name: "NoQuery",
Input: `[[rule]]
name = "randombits"
message = "randombits generates more 1's than 0's in last 5 seconds"
database = "graphite"
category = "testing"
conditions = ["x > 0.5"]
actions = ["Print"]
auto-dismiss = false
`,
ExpectedErr: fmt.Errorf("Alert rule missing field \"query\""),
},
parseCase{
Name: "NoCondition",
Input: `[[rule]]
name = "randombits"
message = "randombits generates more 1's than 0's in last 5 seconds"
database = "graphite"
query = "select mean(value) from random_bits where time > now() - 5s"
category = "testing"
actions = ["Print"]
auto-dismiss = false
`,
ExpectedErr: fmt.Errorf("Alert rule missing field \"conditions\""),
},
parseCase{
Name: "NoActions",
Input: `[[rule]]
name = "randombits"
message = "randombits generates more 1's than 0's in last 5 seconds"
database = "graphite"
query = "select mean(value) from random_bits where time > now() - 5s"
category = "testing"
conditions = ["x > 0.5"]
auto-dismiss = false
`,
ExpectedErr: fmt.Errorf("Alert rule missing field \"actions\""),
},
parseCase{
Name: "BadVariables, 'a' never exists",
Input: `[[rule]]
name = "randombits"
message = "randombits generates more 1's than 0's in last 5 seconds"
database = "graphite"
query = "select mean(value) from random_bits where time > now() - 5s"
category = "testing"
conditions = ["x > a"]
actions = ["Print"]
auto-dismiss = false
`,
ExpectedErr1_6: fmt.Errorf("Failed to evaluate condition \"x > a\": eval:1:5: undeclared name: a"),
// TODO(kjlubick): This behaves differently on golang 1.7 vs 1.6.
// Consolidate this once all the bots are updated.
ExpectedErr: fmt.Errorf("Failed to evaluate condition \"x > a\": Unknown error"),
},
parseCase{
Name: "BadVariables, 'z' isn't defined",
Input: `[[rule]]
name = "randombits"
message = "randombits generates more 1's than 0's in last 5 seconds"
database = "graphite"
query = "select mean(value), sum(value) from random_bits where time > now() - 5s"
category = "testing"
conditions = ["x > y", "x < z"]
actions = ["Print"]
auto-dismiss = false
`,
ExpectedErr: fmt.Errorf("Failed to evaluate condition \"x < z\": eval:1:5: undeclared name: z"),
},
parseCase{
Name: "TooManyVariables",
Input: `[[rule]]
name = "randombits"
message = "randombits generates more 1's than 0's in last 5 seconds"
database = "graphite"
query = "select mean(value), sum(value), min(value), max(value) from random_bits where time > now() - 5s"
category = "testing"
conditions = ["x > y", "x < z"]
actions = ["Print"]
auto-dismiss = false
`,
ExpectedErr: fmt.Errorf(`Too many return values in query "select mean(value), sum(value), min(value), max(value) from random_bits where time > now() - 5s". We only support 3 variables and found 4 return values`),
},
parseCase{
Name: "NoMessage",
Input: `[[rule]]
name = "randombits"
database = "graphite"
query = "select mean(value) from random_bits where time > now() - 5s"
category = "testing"
conditions = ["x > 0.5"]
actions = ["Print"]
auto-dismiss = false
`,
ExpectedErr: fmt.Errorf("Alert rule missing field \"message\""),
},
parseCase{
Name: "NoAutoDismiss",
Input: `[[rule]]
name = "randombits"
message = "randombits generates more 1's than 0's in last 5 seconds"
category = "testing"
database = "graphite"
query = "select mean(value) from random_bits where time > now() - 5s"
conditions = ["x > 0.5"]
actions = ["Print"]
`,
ExpectedErr: fmt.Errorf("Alert rule missing field \"auto-dismiss\""),
},
parseCase{
Name: "Nag",
Input: `[[rule]]
name = "randombits"
message = "randombits generates more 1's than 0's in last 5 seconds"
database = "graphite"
query = "select mean(value) from random_bits where time > now() - 5s"
category = "testing"
conditions = ["x > 0.5"]
actions = ["Print"]
auto-dismiss = false
nag = "1h10m"
`,
ExpectedErr: nil,
},
parseCase{
Name: "NoCategory",
Input: `[[rule]]
name = "randombits"
message = "randombits generates more 1's than 0's in last 5 seconds"
database = "graphite"
query = "select mean(value) from random_bits where time > now() - 5s"
conditions = ["x > 0.5"]
actions = ["Print"]
auto-dismiss = false
nag = "1h10m"
`,
ExpectedErr: fmt.Errorf("Alert rule missing field \"category\""),
},
parseCase{
Name: "NoDatabase",
Input: `[[rule]]
name = "randombits"
message = "randombits generates more 1's than 0's in last 5 seconds"
query = "select mean(value) from random_bits where time > now() - 5s"
category = "testing"
conditions = ["x > 0.5"]
actions = ["Print"]
auto-dismiss = false
nag = "1h10m"
`,
ExpectedErr: fmt.Errorf("Alert rule missing field \"database\""),
},
}
errorStr := "Case %s:\nExpected:\n%v\nActual:\n%v"
for _, c := range cases {
expectedErrStr := "nil"
if c.ExpectedErr != nil {
expectedErrStr = c.ExpectedErr.Error()
}
var cfg struct {
Rule []parsedRule
}
_, err := toml.Decode(c.Input, &cfg)
if err != nil {
t.Errorf("Failed to parse:\n%v", c.Input)
}
_, err = newRule(cfg.Rule[0], nil, false, 10)
actualErrStr := "nil"
if err != nil {
actualErrStr = err.Error()
}
if actualErrStr != expectedErrStr {
if c.ExpectedErr1_6 == nil {
t.Errorf(errorStr, c.Name, expectedErrStr, actualErrStr)
} else {
if expectedError2 := c.ExpectedErr1_6.Error(); actualErrStr != expectedError2 {
t.Errorf(errorStr, c.Name, expectedErrStr+"[ or ]"+expectedError2, actualErrStr)
}
}
}
}
}