blob: 4f0ec7648d8e16fb349821439ab1870b614d0087 [file] [log] [blame]
package continuous
import (
"context"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.skia.org/infra/go/paramtools"
"go.skia.org/infra/go/testutils"
"go.skia.org/infra/perf/go/alerts"
alertconfigmocks "go.skia.org/infra/perf/go/alerts/mock"
"go.skia.org/infra/perf/go/clustering2"
"go.skia.org/infra/perf/go/config"
"go.skia.org/infra/perf/go/dataframe"
"go.skia.org/infra/perf/go/dataframe/mocks"
gitmocks "go.skia.org/infra/perf/go/git/mocks"
"go.skia.org/infra/perf/go/git/provider"
"go.skia.org/infra/perf/go/ingestevents"
notifymocks "go.skia.org/infra/perf/go/notify/mocks"
"go.skia.org/infra/perf/go/regression"
regressionmocks "go.skia.org/infra/perf/go/regression/mocks"
shortcutmocks "go.skia.org/infra/perf/go/shortcut/mocks"
"go.skia.org/infra/perf/go/stepfit"
"go.skia.org/infra/perf/go/types"
"go.skia.org/infra/perf/go/ui/frame"
"golang.org/x/exp/slices"
)
func TestBuildConfigsAndParamSet(t *testing.T) {
mockConfigProvider := alertconfigmocks.NewConfigProvider(t)
mockConfigProvider.On("GetAllAlertConfigs", testutils.AnyContext, false).Return(
[]*alerts.Alert{
{
IDAsString: "1",
},
{
IDAsString: "3",
},
}, nil)
c := Continuous{
provider: mockConfigProvider,
paramsProvider: func() paramtools.ReadOnlyParamSet {
return paramtools.ReadOnlyParamSet{
"config": []string{"8888", "565"},
}
},
pollingDelay: time.Nanosecond,
instanceConfig: &config.InstanceConfig{
DataStoreConfig: config.DataStoreConfig{},
GitRepoConfig: config.GitRepoConfig{},
IngestionConfig: config.IngestionConfig{},
},
flags: &config.FrontendFlags{},
}
// Build channel.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch := c.buildConfigAndParamsetChannel(ctx)
// Read value.
cnp := <-ch
// Confirm it conforms to expectations.
assert.Equal(t, c.paramsProvider(), cnp.paramset)
assert.Len(t, cnp.configs, 2)
ids := []string{}
for _, cfg := range cnp.configs {
ids = append(ids, cfg.IDAsString)
}
assert.Subset(t, []string{"1", "3"}, ids)
// Confirm we continue to get items from the channel.
cnp = <-ch
assert.Equal(t, c.paramsProvider(), cnp.paramset)
}
func TestMatchingConfigsFromTraceIDs_TraceIDSliceIsEmpty_ReturnsEmptySlice(t *testing.T) {
config := alerts.NewConfig()
config.Query = "foo=bar"
traceIDs := []string{}
matchingConfigs := matchingConfigsFromTraceIDs(traceIDs, []*alerts.Alert{config})
require.Empty(t, matchingConfigs)
}
func TestMatchingConfigsFromTraceIDs_OneConfigThatMatchesZeroTraces_ReturnsEmptySlice(t *testing.T) {
config := alerts.NewConfig()
config.Query = "arch=some-unknown-arch"
traceIDs := []string{
",arch=x86,config=8888,",
",arch=arm,config=8888,",
}
matchingConfigs := matchingConfigsFromTraceIDs(traceIDs, []*alerts.Alert{config})
require.Empty(t, matchingConfigs)
}
func TestMatchingConfigsFromTraceIDs_OneConfigThatMatchesOneTrace_ReturnsTheOneConfig(t *testing.T) {
config := alerts.NewConfig()
config.Query = "arch=x86"
traceIDs := []string{
",arch=x86,config=8888,",
",arch=arm,config=8888,",
}
matchingConfigs := matchingConfigsFromTraceIDs(traceIDs, []*alerts.Alert{config})
require.Len(t, matchingConfigs, 1)
}
func TestMatchingConfigsFromTraceIDs_TwoConfigsThatMatchesOneTrace_ReturnsBothConfigs(t *testing.T) {
config1 := alerts.NewConfig()
config1.Query = "arch=x86"
config2 := alerts.NewConfig()
config2.Query = "arch=arm"
traceIDs := []string{
",arch=x86,config=8888,",
",arch=arm,config=8888,",
}
matchingConfigs := matchingConfigsFromTraceIDs(traceIDs, []*alerts.Alert{config1, config2})
require.Len(t, matchingConfigs, 2)
}
func TestMatchingConfigsFromTraceIDs_GroupByMatchesTrace_ReturnsConfigWithRestrictedQuery(t *testing.T) {
config1 := alerts.NewConfig()
config1.Query = "arch=x86"
config1.SetIDFromInt64(123)
config1.GroupBy = "config"
traceIDs := []string{
",arch=x86,config=8888,",
",arch=arm,config=8888,",
}
matchingConfigs := matchingConfigsFromTraceIDs(traceIDs, []*alerts.Alert{config1})
require.Len(t, matchingConfigs, 1)
for config, traces := range matchingConfigs {
assert.Equal(t, config1.IDAsString, config.IDAsString)
assert.Equal(t, config1.GroupBy, config.GroupBy)
// Ensure that the query is updated inside the alert config
assert.NotEqual(t, config1.Query, config.Query)
assert.Equal(t, "arch=x86&config=8888", config.Query)
assert.Equal(t, traceIDs[0], traces[0])
}
}
func TestMatchingConfigsFromTraceIDs_MultipleGroupByPartsMatchTrace_ReturnsConfigWithRestrictedQueryUsingAllMatchingGroupByKeys(t *testing.T) {
config := alerts.NewConfig()
config.Query = "arch=x86"
config.GroupBy = "config,device"
traceIDs := []string{
",arch=x86,config=8888,device=Pixel4,",
",arch=arm,config=8888,device=Pixel4,",
}
matchingConfigs := matchingConfigsFromTraceIDs(traceIDs, []*alerts.Alert{config})
require.Len(t, matchingConfigs, 1)
for config := range matchingConfigs {
assert.Equal(t, "arch=x86&config=8888&device=Pixel4", config.Query)
_, err := url.ParseQuery(config.Query)
require.NoError(t, err)
}
}
type allMocks struct {
perfGit *gitmocks.Git
shortcutStore *shortcutmocks.Store
regressionStore *regressionmocks.Store
notifier *notifymocks.Notifier
dataFrameBuilder *mocks.DataFrameBuilder
configProvider *alertconfigmocks.ConfigProvider
}
func createArgsForReportRegressions(t *testing.T) (*Continuous, *regression.RegressionDetectionRequest, []*regression.RegressionDetectionResponse, *alerts.Alert, allMocks) {
pg := gitmocks.NewGit(t)
ss := shortcutmocks.NewStore(t)
rs := regressionmocks.NewStore(t)
cp := alertconfigmocks.NewConfigProvider(t)
n := notifymocks.NewNotifier(t)
pp := func() paramtools.ReadOnlyParamSet {
return nil
}
dfb := mocks.NewDataFrameBuilder(t)
i := &config.InstanceConfig{}
f := &config.FrontendFlags{}
req := &regression.RegressionDetectionRequest{}
resp := []*regression.RegressionDetectionResponse{}
cfg := &alerts.Alert{}
c := &Continuous{
perfGit: pg,
shortcutStore: ss,
store: rs,
provider: cp,
notifier: n,
paramsProvider: pp,
dfBuilder: dfb,
pollingDelay: time.Microsecond,
instanceConfig: i,
flags: f,
current: &alerts.Alert{},
}
allMocks := allMocks{
perfGit: pg,
shortcutStore: ss,
regressionStore: rs,
notifier: n,
dataFrameBuilder: dfb,
configProvider: cp,
}
return c, req, resp, cfg, allMocks
}
func TestReportRegressions_EmptyRegressionDetectionResponse_NoRegressionsReported(t *testing.T) {
c, req, resp, cfg, _ := createArgsForReportRegressions(t)
// We know this works since we didn't need to supply any implementations for any of the mocks.
c.reportRegressions(context.Background(), req, resp, cfg)
}
func TestReportRegressions_OneNewStepDownRegressionFound_OneRegressionStoredAndNotified(t *testing.T) {
ctx := context.Background()
c, req, resp, cfg, allMocks := createArgsForReportRegressions(t)
const regressionCommitNumber = types.CommitNumber(2)
resp = append(resp, &regression.RegressionDetectionResponse{
Frame: &frame.FrameResponse{
DataFrame: &dataframe.DataFrame{
Header: []*dataframe.ColumnHeader{
{Offset: 1},
{Offset: regressionCommitNumber},
},
ParamSet: paramtools.ReadOnlyParamSet{
"device_name": []string{"sailfish", "sargo", "wembley"},
},
},
},
Summary: &clustering2.ClusterSummaries{
Clusters: []*clustering2.ClusterSummary{
{
Keys: []string{
",device_name=sailfish",
",device_name=sargo",
",device_name=wembley",
},
Shortcut: "some-shortcut-id",
StepFit: &stepfit.StepFit{
Status: stepfit.LOW,
},
StepPoint: &dataframe.ColumnHeader{
Offset: regressionCommitNumber,
},
},
},
},
})
commitAtStep := provider.Commit{
Subject: "The subject of the commit where a regression occurred.",
}
previousCommit := provider.Commit{
Subject: "The subject of the commit right before where a regression occurred.",
}
const notificationID = "some-notification-id"
// First call to CommitFromCommitNumber.
allMocks.perfGit.On("CommitFromCommitNumber", testutils.AnyContext, types.CommitNumber(2)).Return(commitAtStep, nil)
// First call to CommitFromCommitNumber is for the previous commit.
allMocks.perfGit.On("CommitFromCommitNumber", testutils.AnyContext, types.CommitNumber(1)).Return(previousCommit, nil)
cfg.DirectionAsString = alerts.DOWN
// Returns true to indicate that this is a newly found regression. Note that
// this is called twice, first to store the regression since it's new, then
// called again to store the notification ID.
allMocks.regressionStore.On("SetLow", testutils.AnyContext, regressionCommitNumber, cfg.IDAsString, resp[0].Frame, resp[0].Summary.Clusters[0]).Return(true, nil).Twice()
allMocks.notifier.On("RegressionFound", ctx, commitAtStep, previousCommit, cfg, resp[0].Summary.Clusters[0], resp[0].Frame).Return(notificationID, nil)
c.reportRegressions(ctx, req, resp, cfg)
require.Equal(t, notificationID, resp[0].Summary.Clusters[0].NotificationID)
}
func TestTraceIdForIngestEvent_Matching(t *testing.T) {
c, _, _, _, allMocks := createArgsForReportRegressions(t)
allConfigs := []*alerts.Alert{
{
IDAsString: "1",
Query: "&id=trace1&id=trace3",
},
{
IDAsString: "3",
Query: "&id=trace3",
},
}
allMocks.configProvider.On("GetAllAlertConfigs", testutils.AnyContext, false).Return(
allConfigs, nil)
ctx := context.Background()
ie := &ingestevents.IngestEvent{
TraceIDs: []string{",id=trace1,", ",id=trace2,"},
}
configTracesMap, err := c.getTraceIdConfigsForIngestEvent(ctx, ie)
assert.Nil(t, err)
assert.NotNil(t, configTracesMap)
for _, traces := range configTracesMap {
assert.Equal(t, ie.TraceIDs[0], traces[0], "Expect the first config to match first trace.")
assert.False(t, slices.Contains(traces, ie.TraceIDs[1]), "No match expected for second trace.")
}
}
func TestTraceIdForIngestEvent_MultipleConfigs_Matching(t *testing.T) {
c, _, _, _, allMocks := createArgsForReportRegressions(t)
allConfigs := []*alerts.Alert{
{
IDAsString: "1",
Query: "&id=trace1&id=trace3",
},
{
IDAsString: "3",
Query: "&id=trace3",
},
}
allMocks.configProvider.On("GetAllAlertConfigs", testutils.AnyContext, false).Return(
allConfigs, nil)
ctx := context.Background()
ie := &ingestevents.IngestEvent{
TraceIDs: []string{",id=trace3,"},
}
configTracesMap, err := c.getTraceIdConfigsForIngestEvent(ctx, ie)
assert.Nil(t, err)
assert.NotNil(t, configTracesMap)
assert.Equal(t, 2, len(configTracesMap))
}