| 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 := ®ression.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, ®ression.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)) |
| } |