grafana/pkg/services/ngalert/models/alert_rule_test.go
2022-07-04 06:03:34 -04:00

600 lines
16 KiB
Go

package models
import (
"encoding/json"
"math/rand"
"sort"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/grafana/grafana/pkg/util"
)
func TestNoDataStateFromString(t *testing.T) {
allKnownNoDataStates := [...]NoDataState{
Alerting,
NoData,
OK,
}
t.Run("should parse known values", func(t *testing.T) {
for _, state := range allKnownNoDataStates {
stateStr := string(state)
actual, err := NoDataStateFromString(stateStr)
require.NoErrorf(t, err, "failed to parse a known state [%s]", stateStr)
require.Equal(t, state, actual)
}
})
t.Run("should fail to parse in different case", func(t *testing.T) {
for _, state := range allKnownNoDataStates {
stateStr := strings.ToLower(string(state))
actual, err := NoDataStateFromString(stateStr)
require.Errorf(t, err, "expected error for input value [%s]", stateStr)
require.Equal(t, NoDataState(""), actual)
}
})
t.Run("should fail to parse unknown values", func(t *testing.T) {
input := util.GenerateShortUID()
actual, err := NoDataStateFromString(input)
require.Errorf(t, err, "expected error for input value [%s]", input)
require.Equal(t, NoDataState(""), actual)
})
}
func TestErrStateFromString(t *testing.T) {
allKnownErrStates := [...]ExecutionErrorState{
AlertingErrState,
ErrorErrState,
OkErrState,
}
t.Run("should parse known values", func(t *testing.T) {
for _, state := range allKnownErrStates {
stateStr := string(state)
actual, err := ErrStateFromString(stateStr)
require.NoErrorf(t, err, "failed to parse a known state [%s]", stateStr)
require.Equal(t, state, actual)
}
})
t.Run("should fail to parse in different case", func(t *testing.T) {
for _, state := range allKnownErrStates {
stateStr := strings.ToLower(string(state))
actual, err := ErrStateFromString(stateStr)
require.Errorf(t, err, "expected error for input value [%s]", stateStr)
require.Equal(t, ExecutionErrorState(""), actual)
}
})
t.Run("should fail to parse unknown values", func(t *testing.T) {
input := util.GenerateShortUID()
actual, err := ErrStateFromString(input)
require.Errorf(t, err, "expected error for input value [%s]", input)
require.Equal(t, ExecutionErrorState(""), actual)
})
}
func TestPatchPartialAlertRule(t *testing.T) {
t.Run("patches", func(t *testing.T) {
testCases := []struct {
name string
mutator func(r *AlertRule)
}{
{
name: "title is empty",
mutator: func(r *AlertRule) {
r.Title = ""
},
},
{
name: "condition and data are empty",
mutator: func(r *AlertRule) {
r.Condition = ""
r.Data = nil
},
},
{
name: "ExecErrState is empty",
mutator: func(r *AlertRule) {
r.ExecErrState = ""
},
},
{
name: "NoDataState is empty",
mutator: func(r *AlertRule) {
r.NoDataState = ""
},
},
{
name: "For is -1",
mutator: func(r *AlertRule) {
r.For = -1
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
var existing *AlertRule
for {
existing = AlertRuleGen(func(rule *AlertRule) {
rule.For = time.Duration(rand.Int63n(1000) + 1)
})()
cloned := *existing
testCase.mutator(&cloned)
if !cmp.Equal(*existing, cloned, cmp.FilterPath(func(path cmp.Path) bool {
return path.String() == "Data.modelProps"
}, cmp.Ignore())) {
break
}
}
patch := *existing
testCase.mutator(&patch)
require.NotEqual(t, *existing, patch)
PatchPartialAlertRule(existing, &patch)
require.Equal(t, *existing, patch)
})
}
})
t.Run("does not patch", func(t *testing.T) {
testCases := []struct {
name string
mutator func(r *AlertRule)
}{
{
name: "ID",
mutator: func(r *AlertRule) {
r.ID = 0
},
},
{
name: "OrgID",
mutator: func(r *AlertRule) {
r.OrgID = 0
},
},
{
name: "Updated",
mutator: func(r *AlertRule) {
r.Updated = time.Time{}
},
},
{
name: "Version",
mutator: func(r *AlertRule) {
r.Version = 0
},
},
{
name: "UID",
mutator: func(r *AlertRule) {
r.UID = ""
},
},
{
name: "DashboardUID",
mutator: func(r *AlertRule) {
r.DashboardUID = nil
},
},
{
name: "PanelID",
mutator: func(r *AlertRule) {
r.PanelID = nil
},
},
{
name: "Annotations",
mutator: func(r *AlertRule) {
r.Annotations = nil
},
},
{
name: "Labels",
mutator: func(r *AlertRule) {
r.Labels = nil
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
var existing *AlertRule
for {
existing = AlertRuleGen()()
cloned := *existing
// make sure the generated rule does not match the mutated one
testCase.mutator(&cloned)
if !cmp.Equal(*existing, cloned, cmp.FilterPath(func(path cmp.Path) bool {
return path.String() == "Data.modelProps"
}, cmp.Ignore())) {
break
}
}
patch := *existing
testCase.mutator(&patch)
PatchPartialAlertRule(existing, &patch)
require.NotEqual(t, *existing, patch)
})
}
})
}
func TestDiff(t *testing.T) {
t.Run("should return nil if there is no diff", func(t *testing.T) {
rule1 := AlertRuleGen()()
rule2 := CopyRule(rule1)
result := rule1.Diff(rule2)
require.Emptyf(t, result, "expected diff to be empty. rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, result)
})
t.Run("should respect fields to ignore", func(t *testing.T) {
rule1 := AlertRuleGen()()
rule2 := CopyRule(rule1)
rule2.ID = rule1.ID/2 + 1
rule2.Version = rule1.Version/2 + 1
rule2.Updated = rule1.Updated.Add(1 * time.Second)
result := rule1.Diff(rule2, "ID", "Version", "Updated")
require.Emptyf(t, result, "expected diff to be empty. rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, result)
})
t.Run("should find diff in simple fields", func(t *testing.T) {
rule1 := AlertRuleGen()()
rule2 := AlertRuleGen()()
diffs := rule1.Diff(rule2, "Data", "Annotations", "Labels") // these fields will be tested separately
difCnt := 0
if rule1.ID != rule2.ID {
diff := diffs.GetDiffsForField("ID")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.ID, diff[0].Left.Int())
assert.Equal(t, rule2.ID, diff[0].Right.Int())
difCnt++
}
if rule1.OrgID != rule2.OrgID {
diff := diffs.GetDiffsForField("OrgID")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.OrgID, diff[0].Left.Int())
assert.Equal(t, rule2.OrgID, diff[0].Right.Int())
difCnt++
}
if rule1.Title != rule2.Title {
diff := diffs.GetDiffsForField("Title")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.Title, diff[0].Left.String())
assert.Equal(t, rule2.Title, diff[0].Right.String())
difCnt++
}
if rule1.Condition != rule2.Condition {
diff := diffs.GetDiffsForField("Condition")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.Condition, diff[0].Left.String())
assert.Equal(t, rule2.Condition, diff[0].Right.String())
difCnt++
}
if rule1.Updated != rule2.Updated {
diff := diffs.GetDiffsForField("Updated")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.Updated, diff[0].Left.Interface())
assert.Equal(t, rule2.Updated, diff[0].Right.Interface())
difCnt++
}
if rule1.IntervalSeconds != rule2.IntervalSeconds {
diff := diffs.GetDiffsForField("IntervalSeconds")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.IntervalSeconds, diff[0].Left.Int())
assert.Equal(t, rule2.IntervalSeconds, diff[0].Right.Int())
difCnt++
}
if rule1.Version != rule2.Version {
diff := diffs.GetDiffsForField("Version")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.Version, diff[0].Left.Int())
assert.Equal(t, rule2.Version, diff[0].Right.Int())
difCnt++
}
if rule1.UID != rule2.UID {
diff := diffs.GetDiffsForField("UID")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.UID, diff[0].Left.String())
assert.Equal(t, rule2.UID, diff[0].Right.String())
difCnt++
}
if rule1.NamespaceUID != rule2.NamespaceUID {
diff := diffs.GetDiffsForField("NamespaceUID")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.NamespaceUID, diff[0].Left.String())
assert.Equal(t, rule2.NamespaceUID, diff[0].Right.String())
difCnt++
}
if rule1.DashboardUID != rule2.DashboardUID {
diff := diffs.GetDiffsForField("DashboardUID")
assert.Len(t, diff, 1)
difCnt++
}
if rule1.PanelID != rule2.PanelID {
diff := diffs.GetDiffsForField("PanelID")
assert.Len(t, diff, 1)
difCnt++
}
if rule1.RuleGroup != rule2.RuleGroup {
diff := diffs.GetDiffsForField("RuleGroup")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.RuleGroup, diff[0].Left.String())
assert.Equal(t, rule2.RuleGroup, diff[0].Right.String())
difCnt++
}
if rule1.NoDataState != rule2.NoDataState {
diff := diffs.GetDiffsForField("NoDataState")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.NoDataState, diff[0].Left.Interface())
assert.Equal(t, rule2.NoDataState, diff[0].Right.Interface())
difCnt++
}
if rule1.ExecErrState != rule2.ExecErrState {
diff := diffs.GetDiffsForField("ExecErrState")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.ExecErrState, diff[0].Left.Interface())
assert.Equal(t, rule2.ExecErrState, diff[0].Right.Interface())
difCnt++
}
if rule1.For != rule2.For {
diff := diffs.GetDiffsForField("For")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.For, diff[0].Left.Interface())
assert.Equal(t, rule2.For, diff[0].Right.Interface())
difCnt++
}
if rule1.RuleGroupIndex != rule2.RuleGroupIndex {
diff := diffs.GetDiffsForField("RuleGroupIndex")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.RuleGroupIndex, diff[0].Left.Interface())
assert.Equal(t, rule2.RuleGroupIndex, diff[0].Right.Interface())
difCnt++
}
require.Lenf(t, diffs, difCnt, "Got some unexpected diffs. Either add to ignore or add assert to it")
if t.Failed() {
t.Logf("rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, diffs)
}
})
t.Run("should not see difference between nil and empty Annotations", func(t *testing.T) {
rule1 := AlertRuleGen()()
rule1.Annotations = make(map[string]string)
rule2 := CopyRule(rule1)
rule2.Annotations = nil
diff := rule1.Diff(rule2)
require.Empty(t, diff)
})
t.Run("should detect changes in Annotations", func(t *testing.T) {
rule1 := AlertRuleGen()()
rule2 := CopyRule(rule1)
rule1.Annotations = map[string]string{
"key1": "value1",
"key2": "value2",
}
rule2.Annotations = map[string]string{
"key2": "value22",
"key3": "value3",
}
diff := rule1.Diff(rule2)
assert.Len(t, diff, 3)
d := diff.GetDiffsForField("Annotations[key1]")
assert.Len(t, d, 1)
assert.Equal(t, "value1", d[0].Left.String())
assert.False(t, d[0].Right.IsValid())
d = diff.GetDiffsForField("Annotations[key2]")
assert.Len(t, d, 1)
assert.Equal(t, "value2", d[0].Left.String())
assert.Equal(t, "value22", d[0].Right.String())
d = diff.GetDiffsForField("Annotations[key3]")
assert.Len(t, d, 1)
assert.False(t, d[0].Left.IsValid())
assert.Equal(t, "value3", d[0].Right.String())
if t.Failed() {
t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff)
}
})
t.Run("should not see difference between nil and empty Labels", func(t *testing.T) {
rule1 := AlertRuleGen()()
rule1.Annotations = make(map[string]string)
rule2 := CopyRule(rule1)
rule2.Annotations = nil
diff := rule1.Diff(rule2)
require.Empty(t, diff)
})
t.Run("should detect changes in Labels", func(t *testing.T) {
rule1 := AlertRuleGen()()
rule2 := CopyRule(rule1)
rule1.Labels = map[string]string{
"key1": "value1",
"key2": "value2",
}
rule2.Labels = map[string]string{
"key2": "value22",
"key3": "value3",
}
diff := rule1.Diff(rule2)
assert.Len(t, diff, 3)
d := diff.GetDiffsForField("Labels[key1]")
assert.Len(t, d, 1)
assert.Equal(t, "value1", d[0].Left.String())
assert.False(t, d[0].Right.IsValid())
d = diff.GetDiffsForField("Labels[key2]")
assert.Len(t, d, 1)
assert.Equal(t, "value2", d[0].Left.String())
assert.Equal(t, "value22", d[0].Right.String())
d = diff.GetDiffsForField("Labels[key3]")
assert.Len(t, d, 1)
assert.False(t, d[0].Left.IsValid())
assert.Equal(t, "value3", d[0].Right.String())
if t.Failed() {
t.Logf("rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, d)
}
})
t.Run("should detect changes in Data", func(t *testing.T) {
rule1 := AlertRuleGen()()
rule2 := CopyRule(rule1)
query1 := AlertQuery{
RefID: "A",
QueryType: util.GenerateShortUID(),
RelativeTimeRange: RelativeTimeRange{
From: Duration(5 * time.Hour),
To: 0,
},
DatasourceUID: util.GenerateShortUID(),
Model: json.RawMessage(`{ "test": "data"}`),
modelProps: map[string]interface{}{
"test": 1,
},
}
rule1.Data = []AlertQuery{query1}
t.Run("should ignore modelProps", func(t *testing.T) {
query2 := query1
query2.modelProps = map[string]interface{}{
"some": "other value",
}
rule2.Data = []AlertQuery{query2}
diff := rule1.Diff(rule2)
assert.Nil(t, diff)
if t.Failed() {
t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff)
}
})
t.Run("should detect changes inside the query", func(t *testing.T) {
query2 := query1
query2.QueryType = "test"
query2.RefID = "test"
rule2.Data = []AlertQuery{query2}
diff := rule1.Diff(rule2)
assert.Len(t, diff, 2)
d := diff.GetDiffsForField("Data[0].QueryType")
assert.Len(t, d, 1)
d = diff.GetDiffsForField("Data[0].RefID")
assert.Len(t, d, 1)
if t.Failed() {
t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff)
}
})
t.Run("should detect new changes in array if too many fields changed", func(t *testing.T) {
query2 := query1
query2.QueryType = "test"
query2.RefID = "test"
query2.DatasourceUID = "test"
query2.Model = json.RawMessage(`{ "test": "da2ta"}`)
rule2.Data = []AlertQuery{query2}
diff := rule1.Diff(rule2)
assert.Len(t, diff, 2)
for _, d := range diff {
assert.Equal(t, "Data", d.Path)
if d.Left.IsValid() {
assert.Equal(t, query1, d.Left.Interface())
} else {
assert.Equal(t, query2, d.Right.Interface())
}
}
if t.Failed() {
t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff)
}
})
})
}
func TestSortByGroupIndex(t *testing.T) {
ensureNotSorted := func(t *testing.T, rules []*AlertRule, less func(i, j int) bool) {
for i := 0; i < 5; i++ {
rand.Shuffle(len(rules), func(i, j int) {
rules[i], rules[j] = rules[j], rules[i]
})
if !sort.SliceIsSorted(rules, less) {
return
}
}
t.Fatalf("unable to ensure that alerts are not sorted")
}
t.Run("should sort rules by GroupIndex", func(t *testing.T) {
rules := GenerateAlertRules(rand.Intn(15)+5, AlertRuleGen(WithUniqueGroupIndex()))
ensureNotSorted(t, rules, func(i, j int) bool {
return rules[i].RuleGroupIndex < rules[j].RuleGroupIndex
})
RulesGroup(rules).SortByGroupIndex()
require.True(t, sort.SliceIsSorted(rules, func(i, j int) bool {
return rules[i].RuleGroupIndex < rules[j].RuleGroupIndex
}))
})
t.Run("should sort by ID if same GroupIndex", func(t *testing.T) {
rules := GenerateAlertRules(rand.Intn(15)+5, AlertRuleGen(WithUniqueID(), WithGroupIndex(rand.Int())))
ensureNotSorted(t, rules, func(i, j int) bool {
return rules[i].ID < rules[j].ID
})
RulesGroup(rules).SortByGroupIndex()
require.True(t, sort.SliceIsSorted(rules, func(i, j int) bool {
return rules[i].ID < rules[j].ID
}))
})
}
func TestTimeRangeYAML(t *testing.T) {
yamlRaw := "from: 600\nto: 0\n"
var rtr RelativeTimeRange
err := yaml.Unmarshal([]byte(yamlRaw), &rtr)
require.NoError(t, err)
// nanoseconds
require.Equal(t, Duration(600000000000), rtr.From)
require.Equal(t, Duration(0), rtr.To)
serialized, err := yaml.Marshal(rtr)
require.NoError(t, err)
require.Equal(t, yamlRaw, string(serialized))
}