blob: e19f4623df0d455c98e6ce919ccefd46190f32a9 [file]
package stepfit
import (
"testing"
"github.com/stretchr/testify/assert"
"go.skia.org/infra/perf/go/alerts"
"go.skia.org/infra/perf/go/types"
)
func TestEvaluateSimpleRule_ReturnsAnomalyResult(t *testing.T) {
trace := []float32{1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0}
stddevThreshold := float32(0.01)
rule := &alerts.AlgorithmCheck{
Step: types.AbsoluteStep,
Threshold: 2.0,
}
isTriggered, results := evaluateSimpleRule(trace, stddevThreshold, rule)
assert.True(t, isTriggered)
assert.Len(t, results, 1)
assert.Equal(t, string(types.AbsoluteStep), results[0].AlgoName)
assert.Equal(t, float32(4.0), results[0].Value)
assert.Equal(t, float32(2.0), results[0].Threshold)
assert.True(t, results[0].IsAnomaly)
}
func TestEvaluateComplexRule_AND_ReturnsFlattenedResults(t *testing.T) {
trace := []float32{1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0} // A clear step of size 4
stddevThreshold := float32(0.01)
// AND rule: Both should trigger
subrules := []*alerts.AnomalyDetectionRule{
{
SimpleRule: &alerts.AlgorithmCheck{
Step: types.AbsoluteStep,
Threshold: 3.0,
},
},
{
SimpleRule: &alerts.AlgorithmCheck{
Step: types.Const,
Threshold: 1.0,
},
},
}
complexRule := &alerts.ComplexRule{
Op: "AND",
Rules: subrules,
}
rule := &alerts.AnomalyDetectionRule{
ComplexRule: complexRule,
}
stepFit := EvaluateRule(trace, stddevThreshold, rule)
assert.NotNil(t, stepFit)
assert.NotEqual(t, UNINTERESTING, stepFit.Status)
results := stepFit.RuleEvaluations
assert.Len(t, results, 2)
var foundConst, foundAbs bool
for _, res := range results {
if res.AlgoName == string(types.Const) {
foundConst = true
} else if res.AlgoName == string(types.AbsoluteStep) {
foundAbs = true
}
}
assert.True(t, foundConst)
assert.True(t, foundAbs)
}
func TestEvaluateComplexRule_OR_ReturnsFlattenedResults(t *testing.T) {
trace := []float32{1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0} // A clear step of size 4
stddevThreshold := float32(0.01)
// OR rule: one triggers, one does not.
subrules := []*alerts.AnomalyDetectionRule{
{
SimpleRule: &alerts.AlgorithmCheck{
Step: types.AbsoluteStep,
Threshold: 10.0, // Should not trigger (value is 4.0)
},
},
{
SimpleRule: &alerts.AlgorithmCheck{
Step: types.OriginalStep,
Threshold: 0.1, // Should trigger
},
},
}
complexRule := &alerts.ComplexRule{
Op: "OR",
Rules: subrules,
}
rule := &alerts.AnomalyDetectionRule{
ComplexRule: complexRule,
}
stepFit := EvaluateRule(trace, stddevThreshold, rule)
assert.NotNil(t, stepFit)
assert.NotEqual(t, UNINTERESTING, stepFit.Status)
results := stepFit.RuleEvaluations
assert.Len(t, results, 2)
// OriginalStep Value is likely high, let's just assert both are present.
var foundOriginal, foundAbs bool
for _, res := range results {
if res.AlgoName == string(types.OriginalStep) {
foundOriginal = true
assert.True(t, res.IsAnomaly)
} else if res.AlgoName == string(types.AbsoluteStep) {
foundAbs = true
assert.False(t, res.IsAnomaly)
}
}
assert.True(t, foundOriginal)
assert.True(t, foundAbs)
}
func TestEvaluateComplexRule_CohenAndPercent(t *testing.T) {
trace := []float32{10.0, 10.1, 9.9, 20.0, 20.1, 19.9, 20.0}
stddevThreshold := float32(0.01)
rule := &alerts.AnomalyDetectionRule{
ComplexRule: &alerts.ComplexRule{
Op: "AND",
Rules: []*alerts.AnomalyDetectionRule{
{
SimpleRule: &alerts.AlgorithmCheck{
Step: types.PercentStep,
Threshold: 0.5,
},
},
{
SimpleRule: &alerts.AlgorithmCheck{
Step: types.CohenStep,
Threshold: 2.0,
},
},
},
},
}
stepFit := EvaluateRule(trace, stddevThreshold, rule)
assert.NotNil(t, stepFit)
assert.NotEqual(t, UNINTERESTING, stepFit.Status)
results := stepFit.RuleEvaluations
assert.Len(t, results, 2)
var foundPercent, foundCohen bool
for _, res := range results {
if res.AlgoName == string(types.PercentStep) {
foundPercent = true
assert.True(t, res.IsAnomaly)
} else if res.AlgoName == string(types.CohenStep) {
foundCohen = true
assert.True(t, res.IsAnomaly)
}
}
assert.True(t, foundPercent)
assert.True(t, foundCohen)
}
func TestEvaluateComplexRule_CohenAndPercent_AND_Fail(t *testing.T) {
trace := []float32{10.0, 10.1, 9.9, 20.0, 20.1, 19.9, 20.0}
stddevThreshold := float32(0.01)
rule := &alerts.AnomalyDetectionRule{
ComplexRule: &alerts.ComplexRule{
Op: "AND",
Rules: []*alerts.AnomalyDetectionRule{
{
SimpleRule: &alerts.AlgorithmCheck{
Step: types.PercentStep,
Threshold: 1.5, // Should not trigger (value is ~1.0)
},
},
{
SimpleRule: &alerts.AlgorithmCheck{
Step: types.CohenStep,
Threshold: 2.0, // Triggers
},
},
},
},
}
stepFit := EvaluateRule(trace, stddevThreshold, rule)
assert.NotNil(t, stepFit)
assert.Equal(t, UNINTERESTING, stepFit.Status) // Overall false
results := stepFit.RuleEvaluations
assert.Len(t, results, 2)
var foundPercent, foundCohen bool
for _, res := range results {
if res.AlgoName == string(types.PercentStep) {
foundPercent = true
assert.False(t, res.IsAnomaly)
} else if res.AlgoName == string(types.CohenStep) {
foundCohen = true
assert.True(t, res.IsAnomaly)
}
}
assert.True(t, foundPercent)
assert.True(t, foundCohen)
}
func TestEvaluateComplexRule_CohenAndPercent_OR_Success(t *testing.T) {
trace := []float32{10.0, 10.1, 9.9, 20.0, 20.1, 19.9, 20.0}
stddevThreshold := float32(0.01)
rule := &alerts.AnomalyDetectionRule{
ComplexRule: &alerts.ComplexRule{
Op: "OR",
Rules: []*alerts.AnomalyDetectionRule{
{
SimpleRule: &alerts.AlgorithmCheck{
Step: types.PercentStep,
Threshold: 1.5, // Should not trigger
},
},
{
SimpleRule: &alerts.AlgorithmCheck{
Step: types.CohenStep,
Threshold: 2.0, // Triggers
},
},
},
},
}
stepFit := EvaluateRule(trace, stddevThreshold, rule)
assert.NotNil(t, stepFit)
assert.NotEqual(t, UNINTERESTING, stepFit.Status) // Overall true
results := stepFit.RuleEvaluations
assert.Len(t, results, 2)
var foundPercent, foundCohen bool
for _, res := range results {
if res.AlgoName == string(types.PercentStep) {
foundPercent = true
assert.False(t, res.IsAnomaly)
} else if res.AlgoName == string(types.CohenStep) {
foundCohen = true
assert.True(t, res.IsAnomaly)
}
}
assert.True(t, foundPercent)
assert.True(t, foundCohen)
}
func TestGetWindowSize_And_UsesOriginalStep(t *testing.T) {
assert.True(t, UsesOriginalStep(types.OriginalStep, nil))
assert.False(t, UsesOriginalStep(types.PercentStep, nil))
assert.False(t, UsesOriginalStep(types.CohenStep, nil))
assert.Equal(t, 7, GetWindowSize(3, types.OriginalStep, nil))
assert.Equal(t, 6, GetWindowSize(3, types.PercentStep, nil))
assert.Equal(t, 6, GetWindowSize(3, types.CohenStep, nil))
complexWithOriginal := &alerts.AnomalyDetectionRule{
ComplexRule: &alerts.ComplexRule{
Op: "OR",
Rules: []*alerts.AnomalyDetectionRule{
NewSimpleRule(types.CohenStep, 2.0),
NewSimpleRule(types.OriginalStep, 0.5),
},
},
}
assert.True(t, UsesOriginalStep(types.CohenStep, complexWithOriginal))
assert.Equal(t, 7, GetWindowSize(3, types.CohenStep, complexWithOriginal))
}
func TestEvaluateComplexRule_OriginalAndPercent_OR(t *testing.T) {
// 7 elements: OriginalStep uses all 7; PercentStep drops the 7th and uses first 6.
trace := []float32{10.0, 10.0, 10.0, 20.0, 20.0, 20.0, 20.0}
stddevThreshold := float32(0.01)
rule := &alerts.AnomalyDetectionRule{
ComplexRule: &alerts.ComplexRule{
Op: "OR",
Rules: []*alerts.AnomalyDetectionRule{
NewSimpleRule(types.OriginalStep, 0.5),
NewSimpleRule(types.PercentStep, 0.5),
},
},
}
stepFit := EvaluateRule(trace, stddevThreshold, rule)
assert.NotNil(t, stepFit)
assert.NotEqual(t, UNINTERESTING, stepFit.Status)
results := stepFit.RuleEvaluations
assert.Len(t, results, 2)
var foundOriginal, foundPercent bool
for _, res := range results {
if res.AlgoName == string(types.OriginalStep) {
foundOriginal = true
assert.True(t, res.IsAnomaly)
} else if res.AlgoName == string(types.PercentStep) {
foundPercent = true
assert.True(t, res.IsAnomaly)
}
}
assert.True(t, foundOriginal)
assert.True(t, foundPercent)
}
func TestMinSizeForAlgorithm(t *testing.T) {
assert.Equal(t, 3, MinSizeForAlgorithm(types.OriginalStep))
assert.Equal(t, 4, MinSizeForAlgorithm(types.CohenStep))
assert.Equal(t, 2, MinSizeForAlgorithm(types.PercentStep))
assert.Equal(t, 2, MinSizeForAlgorithm(types.PercentMedianStep))
assert.Equal(t, 2, MinSizeForAlgorithm(types.AbsoluteStep))
assert.Equal(t, 2, MinSizeForAlgorithm(types.Const))
assert.Equal(t, 2, MinSizeForAlgorithm(types.Stepiness))
assert.Equal(t, 2, MinSizeForAlgorithm(types.MannWhitneyU))
}
func TestEvaluateSimpleRule_TooShort_ReturnsFalse(t *testing.T) {
stddevThreshold := float32(0.01)
for _, step := range types.AllStepDetections {
name := string(step)
if name == "" {
name = "OriginalStep"
}
t.Run(name, func(t *testing.T) {
minSize := MinSizeForAlgorithm(step)
tooShortTrace := make([]float32, minSize-1)
rule := &alerts.AlgorithmCheck{
Step: step,
Threshold: 0.5,
}
isTriggered, results := evaluateSimpleRule(tooShortTrace, stddevThreshold, rule)
assert.False(t, isTriggered)
assert.Nil(t, results)
})
}
}
func TestEvaluateSimpleRule_LongEnough_Evaluates(t *testing.T) {
stddevThreshold := float32(0.01)
for _, step := range types.AllStepDetections {
name := string(step)
if name == "" {
name = "OriginalStep"
}
t.Run(name, func(t *testing.T) {
minSize := MinSizeForAlgorithm(step)
trace := make([]float32, minSize)
for i := range trace {
trace[i] = float32(i)
}
rule := &alerts.AlgorithmCheck{
Step: step,
Threshold: 0.5,
}
_, results := evaluateSimpleRule(trace, stddevThreshold, rule)
assert.NotNil(t, results, "Should have evaluated for step %s with length %d", step, len(trace))
})
}
}