blob: 243a8f3cd630ac4d2e13e9a00cfa28ed07aeca85 [file] [log] [blame]
package sqlregression2store
import (
"context"
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/jackc/pgconn"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.skia.org/infra/perf/go/alerts"
alerts_mock "go.skia.org/infra/perf/go/alerts/mock"
"go.skia.org/infra/perf/go/clustering2"
"go.skia.org/infra/perf/go/dataframe"
"go.skia.org/infra/perf/go/regression"
"go.skia.org/infra/perf/go/sql/sqltest"
"go.skia.org/infra/perf/go/stepfit"
"go.skia.org/infra/perf/go/types"
"go.skia.org/infra/perf/go/ui/frame"
)
const alertId int64 = 1111
func setupStore(t *testing.T, alertsProvider alerts.ConfigProvider) *SQLRegression2Store {
db := sqltest.NewSpannerDBForTests(t, "regstore")
store, _ := New(db, alertsProvider)
return store
}
func readSpecificRegressionFromDb(ctx context.Context, t *testing.T, store *SQLRegression2Store, commitNumber types.CommitNumber, alertIdStr string) *regression.Regression {
regressionsFromDb, err := store.Range(ctx, commitNumber, commitNumber)
assert.Nil(t, err)
reg := regressionsFromDb[commitNumber].ByAlertID[alertIdStr]
return reg
}
func generateNewRegression() *regression.Regression {
r := regression.NewRegression()
r.Id = uuid.NewString()
r.CommitNumber = 12345
r.AlertId = alertId
r.Bugs = []regression.RegressionBug{}
r.CreationTime = time.Now()
r.IsImprovement = false
r.MedianBefore = 1.0
r.MedianAfter = 2.0
r.PrevCommitNumber = 12340
df := &frame.FrameResponse{
DataFrame: &dataframe.DataFrame{
Header: []*dataframe.ColumnHeader{
{Offset: 1},
{Offset: 2},
{Offset: 3},
},
},
}
clusterSummary := &clustering2.ClusterSummary{
StepFit: &stepfit.StepFit{
TurningPoint: 1,
},
Timestamp: time.Now(),
Centroid: []float32{1.0, 5.0, 5.0},
}
r.High = clusterSummary
r.Frame = df
return r
}
func generateAndStoreNewRegression(ctx context.Context, t *testing.T, store *SQLRegression2Store) *regression.Regression {
r := generateNewRegression()
_, err := store.WriteRegression(ctx, r, nil)
assert.Nil(t, err)
return r
}
func assertRegression(t *testing.T, expected *regression.Regression, actual *regression.Regression) {
assert.Equal(t, expected.AlertId, actual.AlertId)
assert.Equal(t, expected.CommitNumber, actual.CommitNumber)
assert.Equal(t, expected.PrevCommitNumber, actual.PrevCommitNumber)
assert.Equal(t, expected.IsImprovement, actual.IsImprovement)
assert.Equal(t, expected.MedianBefore, actual.MedianBefore)
assert.Equal(t, expected.MedianAfter, actual.MedianAfter)
assert.Equal(t, expected.Frame, actual.Frame)
}
// TestWriteRead_Success writes a regression to the database
// and verifies if it is read back correctly.
func TestWriteRead_Success(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
store := setupStore(t, alertsProvider)
ctx := context.Background()
r := generateAndStoreNewRegression(ctx, t, store)
regressionsFromDb, err := store.Range(ctx, r.CommitNumber, r.CommitNumber)
assert.Nil(t, err)
assert.NotNil(t, regressionsFromDb)
regressionsForCommit := regressionsFromDb[r.CommitNumber]
assert.NotNil(t, regressionsForCommit)
regressionByAlertId := regressionsForCommit.ByAlertID[alerts.IDToString(r.AlertId)]
assert.NotNil(t, regressionByAlertId)
assertRegression(t, r, regressionByAlertId)
}
// TestRead_Empty reads the database when it is empty
func TestRead_Empty(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
store := setupStore(t, alertsProvider)
ctx := context.Background()
// Try reading items when db is empty.
regressionsFromDb, err := store.Range(ctx, 1, 2)
assert.Nil(t, err)
assert.Empty(t, regressionsFromDb)
// Now let's add an item and try to read non-existent items.
r := generateAndStoreNewRegression(ctx, t, store)
regressionsFromDb, err = store.Range(ctx, r.CommitNumber+1, r.CommitNumber+2)
assert.Nil(t, err)
assert.Empty(t, regressionsFromDb)
}
// TestGetByIDs_Success reads the database using the
// ids of the created regressions.
func TestGetByIDs_Success(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
store := setupStore(t, alertsProvider)
ctx := context.Background()
r := generateAndStoreNewRegression(ctx, t, store)
r2 := generateAndStoreNewRegression(ctx, t, store)
// Improvements are anomalies, and they are stored, too.
rImprovement := generateNewRegression()
populateRegression2Fields(rImprovement)
rImprovement.IsImprovement = true
err := store.writeSingleRegression(ctx, rImprovement, nil)
assert.Nil(t, err)
tests := []struct {
name string
regressionIDs []string
expectedLen int
shouldContainIDs []string
}{
{
name: "two regressions",
regressionIDs: []string{r.Id, r2.Id},
expectedLen: 2,
shouldContainIDs: []string{r.Id, r2.Id},
},
{
name: "two regressions and one improvement",
regressionIDs: []string{r.Id, r2.Id, rImprovement.Id},
expectedLen: 3,
shouldContainIDs: []string{r.Id, r2.Id, rImprovement.Id},
},
{
name: "empty ids list",
regressionIDs: []string{},
expectedLen: 0,
shouldContainIDs: []string{},
},
{
name: "just the improvement",
regressionIDs: []string{rImprovement.Id},
expectedLen: 1,
shouldContainIDs: []string{rImprovement.Id},
},
{
name: "duplicate ids are ignored",
regressionIDs: []string{rImprovement.Id, rImprovement.Id, rImprovement.Id, r.Id, r.Id},
expectedLen: 2,
shouldContainIDs: []string{rImprovement.Id, r.Id},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
regressions, err := store.GetByIDs(ctx, tc.regressionIDs)
assert.NoError(t, err)
assert.Equal(t, tc.expectedLen, len(regressions))
for _, r := range regressions {
assert.Contains(t, tc.shouldContainIDs, r.Id)
}
})
}
}
// TestGetByIDs_Success reads the database using the
// ids of the created regressions.
func TestGetByRevision_Success(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
store := setupStore(t, alertsProvider)
ctx := context.Background()
generateRegression := func(previousCommit int64, commit int64) (r *regression.Regression) {
r = generateNewRegression()
populateRegression2Fields(r)
r.PrevCommitNumber = types.CommitNumber(previousCommit)
r.CommitNumber = types.CommitNumber(commit)
err := store.writeSingleRegression(ctx, r, nil)
require.NoError(t, err)
return
}
r100_200 := generateRegression(100, 200)
r101_200 := generateRegression(101, 200)
r300_301 := generateRegression(300, 301)
tests := []struct {
name string
revision string
shouldContainIDs []string
}{
{
name: "revision inside two regressions",
revision: "102",
shouldContainIDs: []string{r100_200.Id, r101_200.Id},
},
{
name: "inside a regression and at the beginning of another",
revision: "101",
shouldContainIDs: []string{r100_200.Id},
},
{
name: "beginning of a regression and before others",
revision: "100",
shouldContainIDs: []string{},
},
{
name: "before all regressions",
revision: "99",
shouldContainIDs: []string{},
},
{
name: "just before the end of regressions",
revision: "199",
shouldContainIDs: []string{r100_200.Id, r101_200.Id},
},
{
name: "coinciding with the commit number of two regressions",
revision: "200",
shouldContainIDs: []string{r100_200.Id, r101_200.Id},
},
{
name: "right after some regressions",
revision: "201",
shouldContainIDs: []string{},
},
{
name: "inside a 1-wide regression",
revision: "301",
shouldContainIDs: []string{r300_301.Id},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
regressions, err := store.GetByRevision(ctx, tc.revision)
assert.NoError(t, err)
assert.Equal(t, len(tc.shouldContainIDs), len(regressions))
for _, r := range regressions {
assert.Contains(t, tc.shouldContainIDs, r.Id)
}
})
}
}
// TestHighRegression_KMeans_Triage sets a High regression into the database, triages it
// and verifies that the data was updated correctly. The alert Algo is set to be KMeans.
func TestHighRegression_KMeans_Triage(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
alertsProvider.On("GetAlertConfig", alertId).Return(&alerts.Alert{
IDAsString: "1111",
DisplayName: "Test Alert Config",
Algo: types.KMeansGrouping,
}, nil)
runClusterSummaryAndTriageTest(t, true, alertsProvider)
}
// TestLowRegression_KMeans_Triage sets a Low regression into the database, triages it
// and verifies that the data was updated correctly. The alert Algo is set to be KMeans.
func TestLowRegression_KMeans_Triage(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
alertsProvider.On("GetAlertConfig", alertId).Return(&alerts.Alert{
IDAsString: "1111",
DisplayName: "Test Alert Config",
Algo: types.KMeansGrouping,
}, nil)
runClusterSummaryAndTriageTest(t, false, alertsProvider)
}
// TestHighRegression_Ind_Triage sets a High regression into the database, triages it
// and verifies that the data was updated correctly. The alert Algo is set to be
// StepFitGrouping (i.e Individual)
func TestHighRegression_Ind_Triage(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
alertsProvider.On("GetAlertConfig", alertId).Return(&alerts.Alert{
IDAsString: "1111",
DisplayName: "Test Alert Config",
Algo: types.StepFitGrouping,
}, nil)
runClusterSummaryAndTriageTest(t, true, alertsProvider)
}
// TestLowRegression_Ind_Triage sets a Low regression into the database, triages it
// and verifies that the data was updated correctly. The alert Algo is set to be
// StepFitGrouping (i.e Individual)
func TestLowRegression_Ind_Triage(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
alertsProvider.On("GetAlertConfig", alertId).Return(&alerts.Alert{
IDAsString: "1111",
DisplayName: "Test Alert Config",
Algo: types.StepFitGrouping,
}, nil)
runClusterSummaryAndTriageTest(t, false, alertsProvider)
}
func TestMixedRegressionWrite(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
alertIdStr := "1111"
store := setupStore(t, alertsProvider)
ctx := context.Background()
// Add an item to the database.
r := generateNewRegression()
r.Id = ""
// Add another cluster summary to the same regression.
r.Low = r.High
_, err := store.WriteRegression(ctx, r, nil)
assert.Nil(t, err)
reg := readSpecificRegressionFromDb(ctx, t, store, r.CommitNumber, alertIdStr)
assert.NotNil(t, reg)
assert.NotNil(t, reg.High)
assert.NotNil(t, reg.Low)
}
func TestRangeFiltered(t *testing.T) {
const (
traceKey1 = ",benchmark=Blazor,bot=MacM1,master=ChromiumPerf,test=test1,"
traceKey2 = ",benchmark=Blazor,bot=MacM1,master=ChromiumPerf,test=test2,"
nonExistentTraceKey = "non-existent-trace"
)
alertsProvider := alerts_mock.NewConfigProvider(t)
store := setupStore(t, alertsProvider)
ctx := context.Background()
// Add a regression with trace key 1.
r1 := generateNewRegression()
r1.CommitNumber = 12345
r1.Frame.DataFrame.TraceSet = types.TraceSet{traceKey1: {}}
_, err := store.WriteRegression(ctx, r1, nil)
assert.Nil(t, err)
// Add a regression with trace key 2.
r2 := generateNewRegression()
r2.CommitNumber = 12346
r2.Frame.DataFrame.TraceSet = types.TraceSet{traceKey2: {}}
_, err = store.WriteRegression(ctx, r2, nil)
assert.Nil(t, err)
// Filter by trace key 1.
regressionsFromDb, err := store.RangeFiltered(ctx, r1.CommitNumber, r1.CommitNumber, []string{traceKey1})
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
if strings.Contains(pgErr.Message, "Postgres function jsonb_exists_any(jsonb, text[]) is not supported") {
// TODO(ansid): this can be removed when Spanner emulator image in gcloudsdk is updated.
// To test if it can be removed already, remove and run tests with "--config=remote".
t.Skip("Skiped test unsupported by Spanner emulator")
return
}
}
}
assert.Nil(t, err)
assert.NotNil(t, regressionsFromDb)
assert.Len(t, regressionsFromDb, 1)
assertRegression(t, r1, regressionsFromDb[0])
// Filter by trace key 2.
regressionsFromDb, err = store.RangeFiltered(ctx, r2.CommitNumber, r2.CommitNumber, []string{traceKey2})
assert.Nil(t, err)
assert.NotNil(t, regressionsFromDb)
assert.Len(t, regressionsFromDb, 1)
assertRegression(t, r2, regressionsFromDb[0])
// Filter by both trace keys.
regressionsFromDb, err = store.RangeFiltered(ctx, r1.CommitNumber, r2.CommitNumber, []string{traceKey1, traceKey2})
assert.Nil(t, err)
assert.NotNil(t, regressionsFromDb)
assert.Len(t, regressionsFromDb, 2)
// Filter by a non-existent trace key.
regressionsFromDb, err = store.RangeFiltered(ctx, r1.CommitNumber, r2.CommitNumber, []string{nonExistentTraceKey})
assert.Nil(t, err)
assert.Empty(t, regressionsFromDb)
}
func runClusterSummaryAndTriageTest(t *testing.T, isHighRegression bool, alertsProvider alerts.ConfigProvider) {
store := setupStore(t, alertsProvider)
ctx := context.Background()
// Add an item to the database.
r := generateNewRegression()
alertIdStr := alerts.IDToString(r.AlertId)
clusterSummary := &clustering2.ClusterSummary{
Centroid: []float32{1.0, 2.0, 3.0},
StepFit: &stepfit.StepFit{
TurningPoint: 1,
},
}
var success bool
var err error
frameResponse := &frame.FrameResponse{
DataFrame: &dataframe.DataFrame{
Header: []*dataframe.ColumnHeader{
{
Offset: 1,
},
{
Offset: 2,
},
{
Offset: 3,
},
},
},
}
if isHighRegression {
// Set a high regression.
success, _, err = store.SetHigh(ctx, r.CommitNumber, alertIdStr, frameResponse, clusterSummary)
} else {
// Set a low regression.
success, _, err = store.SetLow(ctx, r.CommitNumber, alertIdStr, frameResponse, clusterSummary)
}
assert.Nil(t, err)
assert.True(t, success)
// Read the regression and verify that High value was set correctly.
reg := readSpecificRegressionFromDb(ctx, t, store, r.CommitNumber, alertIdStr)
assert.NotNil(t, reg)
if isHighRegression {
assert.NotNil(t, reg.High)
assert.Nil(t, reg.Low)
assert.Equal(t, clusterSummary, reg.High)
} else {
assert.NotNil(t, reg.Low)
assert.Nil(t, reg.High)
assert.Equal(t, clusterSummary, reg.Low)
}
triageStatus := regression.TriageStatus{
Status: regression.Negative,
Message: "Test",
}
// Set the triage value in the db.
if isHighRegression {
err = store.TriageHigh(ctx, r.CommitNumber, alertIdStr, triageStatus)
} else {
err = store.TriageLow(ctx, r.CommitNumber, alertIdStr, triageStatus)
}
assert.Nil(t, err)
// Now read the regression and verify that this value was applied correctly.
reg = readSpecificRegressionFromDb(ctx, t, store, r.CommitNumber, alertIdStr)
if isHighRegression {
assert.Equal(t, triageStatus, reg.HighStatus)
assert.Equal(t, regression.TriageStatus{}, reg.LowStatus)
} else {
assert.Equal(t, triageStatus, reg.LowStatus)
assert.Equal(t, regression.TriageStatus{}, reg.HighStatus)
}
}
// TestGetRegressionsBySubName tests the GetRegressionsBySubName method.
func TestGetRegressionsBySubName(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
store := setupStore(t, alertsProvider)
ctx := context.Background()
// 1. Setup: Insert two regressions to test sorting and pagination.
r1 := generateAndStoreNewRegression(ctx, t, store)
r2 := generateAndStoreNewRegression(ctx, t, store)
// Ensure r1 is older than r2.
olderTime := time.Now().Add(-1 * time.Hour)
_, err := store.db.Exec(ctx, "UPDATE Regressions2 SET creation_time = $1 WHERE id = $2", olderTime, r1.Id)
require.NoError(t, err)
// 2. Associate both regressions with the same subscription (sub_name).
setupAlertSubName := func(alertID int64, subName string) {
query := `
INSERT INTO Alerts (id, sub_name)
VALUES ($1, $2)
ON CONFLICT (id)
DO UPDATE SET
id = EXCLUDED.id,
sub_name = EXCLUDED.sub_name`
_, err := store.db.Exec(ctx, query, alertID, subName)
require.NoError(t, err)
}
setupAlertSubName(r1.AlertId, "my-sub")
setupAlertSubName(r2.AlertId, "my-sub")
// 3. Test cases.
tests := []struct {
name string
subName string
limit int
offset int
expectedLen int
// expectedIDs is used to verify the exact order of return
expectedIDs []string
}{
{
name: "happy path - get all (newest first)",
subName: "my-sub",
limit: 10,
offset: 0,
expectedLen: 2,
expectedIDs: []string{r2.Id, r1.Id}, // Expect r2 (newer) then r1 (older)
},
{
name: "pagination - limit 1 (get newest)",
subName: "my-sub",
limit: 1,
offset: 0,
expectedLen: 1,
expectedIDs: []string{r2.Id},
},
{
name: "pagination - offset 1 (get older)",
subName: "my-sub",
limit: 10,
offset: 1,
expectedLen: 1,
expectedIDs: []string{r1.Id},
},
{
name: "no regressions for sub name",
subName: "non-existent-sub",
limit: 10,
offset: 0,
expectedLen: 0,
expectedIDs: []string{},
},
{
name: "limit 0 returns nothing",
subName: "my-sub",
limit: 0,
offset: 0,
expectedLen: 0,
expectedIDs: []string{},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
regs, err := store.GetRegressionsBySubName(ctx, tc.subName, tc.limit, tc.offset)
require.NoError(t, err)
assert.Len(t, regs, tc.expectedLen)
// Verify the order
if tc.expectedLen > 0 {
for i, expectedID := range tc.expectedIDs {
assert.Equal(t, expectedID, regs[i].Id, "Regression at index %d did not match expected ID", i)
}
}
})
}
}
func TestSetBugID_Success(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
store := setupStore(t, alertsProvider)
ctx := context.Background()
// Insert some regressions to update.
regressions := []*regression.Regression{
generateNewRegression(),
generateNewRegression(),
generateNewRegression(),
}
regIDs := []string{}
for _, reg := range regressions {
_, err := store.WriteRegression(ctx, reg, nil)
require.NoError(t, err)
regIDs = append(regIDs, reg.Id)
}
bugID := 12345
idsToUpdate := []string{regIDs[0], regIDs[1]}
err := store.SetBugID(ctx, idsToUpdate, bugID)
require.NoError(t, err)
// Verify that the bug_id was updated for reg1 and reg2.
for _, id := range idsToUpdate {
regs, err := store.GetByIDs(ctx, []string{id})
require.NoError(t, err)
require.Len(t, regs, 1)
assert.Equal(t, 1, len(regs[0].Bugs))
assert.Equal(t, fmt.Sprint(bugID), regs[0].Bugs[0].BugId)
assert.Equal(t, regression.Negative, regs[0].HighStatus.Status)
}
// Verify that bug_id was not updated for reg3.
regs, err := store.GetByIDs(ctx, []string{regIDs[2]})
require.NoError(t, err)
require.Len(t, regs, 1)
assert.Equal(t, 0, len(regs[0].Bugs))
assert.NotEqual(t, regression.Negative, regs[0].HighStatus.Status)
}
func TestSetBugID_NoIDs(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
store := setupStore(t, alertsProvider)
ctx := context.Background()
err := store.SetBugID(ctx, []string{}, 12345)
require.NoError(t, err)
}
func TestResetAnomalies_Success(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
store := setupStore(t, alertsProvider)
ctx := context.Background()
// Insert some regressions to update.
regressions := []*regression.Regression{
generateNewRegression(),
generateNewRegression(),
generateNewRegression(),
}
bugIdDefault := int64(1)
statusDefault := regression.Negative
messageDefault := "foo"
regIDs := []string{}
for _, reg := range regressions {
// set the following fields to see if reset works.
reg.Bugs = []regression.RegressionBug{{BugId: fmt.Sprint(bugIdDefault), Type: regression.ManualTriage}}
reg.HighStatus.Status = statusDefault
reg.HighStatus.Message = messageDefault
_, err := store.WriteRegression(ctx, reg, nil)
require.NoError(t, err)
regIDs = append(regIDs, reg.Id)
}
idsToUpdate := []string{regIDs[0], regIDs[1]}
err := store.ResetAnomalies(ctx, idsToUpdate)
require.NoError(t, err)
// Verify that the bug_id and triage status were updated for reg1 and reg2.
for _, id := range idsToUpdate {
regs, err := store.GetByIDs(ctx, []string{id})
require.NoError(t, err)
require.Len(t, regs, 1)
assert.Equal(t, 0, len(regs[0].Bugs))
// generateNewRegression sets High, so we expect HighStatus to be updated.
assert.Equal(t, regression.Untriaged, regs[0].HighStatus.Status)
assert.Equal(t, regression.ResetMessage, regs[0].HighStatus.Message)
}
// Verify that nothing was updated for reg3.
regs, err := store.GetByIDs(ctx, []string{regIDs[2]})
require.NoError(t, err)
require.Len(t, regs, 1)
assert.Equal(t, 1, len(regs[0].Bugs))
assert.Equal(t, fmt.Sprint(1), regs[0].Bugs[0].BugId)
assert.NotEqual(t, regression.Untriaged, regs[0].HighStatus.Status)
assert.Equal(t, statusDefault, regs[0].HighStatus.Status)
assert.Equal(t, messageDefault, regs[0].HighStatus.Message)
}
func TestIgnoreAnomalies_Success(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
store := setupStore(t, alertsProvider)
ctx := context.Background()
// Insert some regressions to update.
regressions := []*regression.Regression{
generateNewRegression(),
generateNewRegression(),
generateNewRegression(),
}
regIDs := []string{}
for _, reg := range regressions {
_, err := store.WriteRegression(ctx, reg, nil)
require.NoError(t, err)
regIDs = append(regIDs, reg.Id)
}
idsToUpdate := []string{regIDs[0], regIDs[1]}
err := store.IgnoreAnomalies(ctx, idsToUpdate)
require.NoError(t, err)
// Verify that the triage status was updated for reg1 and reg2.
for _, id := range idsToUpdate {
regs, err := store.GetByIDs(ctx, []string{id})
require.NoError(t, err)
require.Len(t, regs, 1)
// generateNewRegression sets High, so we expect HighStatus to be updated.
assert.Equal(t, regression.Ignored, regs[0].HighStatus.Status)
assert.Equal(t, regression.IgnoredMessage, regs[0].HighStatus.Message)
}
// Verify that nothing was updated for reg3.
regs, err := store.GetByIDs(ctx, []string{regIDs[2]})
require.NoError(t, err)
require.Len(t, regs, 1)
assert.NotEqual(t, regression.Ignored, regs[0].HighStatus.Status)
}
func TestNudgeAndResetAnomalies_ResetsStatus(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
store := setupStore(t, alertsProvider)
ctx := context.Background()
// Insert some regressions to update.
regressions := []*regression.Regression{
generateNewRegression(),
generateNewRegression(),
generateNewRegression(),
}
// store.TriageHigh sets status and message on all regressions with the same commit number and alert id.
// That's why we change this regression to be on a different commit number.
regressions[2].CommitNumber = regressions[2].CommitNumber + 1
bugIdDefault := int64(1)
statusDefault := regression.Negative
messageDefault := "foo"
regIDs := []string{}
for _, reg := range regressions {
reg.Bugs = []regression.RegressionBug{{BugId: fmt.Sprint(bugIdDefault), Type: regression.ManualTriage}}
reg.HighStatus.Status = statusDefault
reg.HighStatus.Message = messageDefault
_, err := store.WriteRegression(ctx, reg, nil)
require.NoError(t, err)
regIDs = append(regIDs, reg.Id)
}
// Set a bug ID and triage status for the first regression to verify it gets reset.
err := store.SetBugID(ctx, []string{regIDs[0]}, 12345)
require.NoError(t, err)
err = store.TriageHigh(ctx, regressions[0].CommitNumber, alerts.IDToString(regressions[0].AlertId), regression.TriageStatus{Status: regression.Positive, Message: "foo"})
require.NoError(t, err)
idsToUpdate := []string{regIDs[0], regIDs[1]}
newCommitNumber := types.CommitNumber(100)
newPrevCommitNumber := types.CommitNumber(90)
err = store.NudgeAndResetAnomalies(ctx, idsToUpdate, newCommitNumber, newPrevCommitNumber)
require.NoError(t, err)
// Verify that the bug_id and triage status were updated for reg1 and reg2.
for _, id := range idsToUpdate {
regs, err := store.GetByIDs(ctx, []string{id})
require.NoError(t, err)
require.Len(t, regs, 1)
assert.Equal(t, 0, len(regs[0].Bugs))
assert.Equal(t, newCommitNumber, regs[0].CommitNumber)
assert.Equal(t, newPrevCommitNumber, regs[0].PrevCommitNumber)
// generateNewRegression sets High, so we expect HighStatus to be updated.
assert.Equal(t, regression.Untriaged, regs[0].HighStatus.Status)
assert.Equal(t, regression.NudgedMessage, regs[0].HighStatus.Message)
}
// Verify that nothing was updated for reg3.
regs, err := store.GetByIDs(ctx, []string{regIDs[2]})
require.NoError(t, err)
require.Len(t, regs, 1)
assert.Equal(t, 1, len(regs[0].Bugs))
assert.Equal(t, fmt.Sprint(bugIdDefault), regs[0].Bugs[0].BugId)
assert.Equal(t, statusDefault, regs[0].HighStatus.Status)
assert.Equal(t, messageDefault, regs[0].HighStatus.Message)
}
func TestGetSubscriptionsForRegressions(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
store := setupStore(t, alertsProvider)
ctx := context.Background()
alertId1 := int64(1)
alertId2 := int64(2)
alertId3 := int64(3)
component1 := "123456"
component2 := "123467"
// 1. Setup: Insert regressions, alerts, and subscriptions.
reg1 := generateNewRegression()
reg1.AlertId = alertId1
_, err := store.WriteRegression(ctx, reg1, nil)
assert.Nil(t, err)
reg2 := generateNewRegression()
reg2.AlertId = alertId2
_, err = store.WriteRegression(ctx, reg2, nil)
assert.Nil(t, err)
reg3WithoutSubscription := generateNewRegression()
reg3WithoutSubscription.AlertId = alertId3
_, err = store.WriteRegression(ctx, reg3WithoutSubscription, nil)
assert.Nil(t, err)
// Setup Alerts
setupAlert := func(alertID int64, subName string, subRevision string) {
query := `
INSERT INTO Alerts (id, sub_name, sub_revision)
VALUES ($1, $2, $3)
ON CONFLICT (id)
DO UPDATE SET
id = EXCLUDED.id,
sub_name = EXCLUDED.sub_name,
sub_revision = EXCLUDED.sub_revision`
_, err := store.db.Exec(ctx, query, alertID, subName, subRevision)
require.NoError(t, err)
}
setupAlert(reg1.AlertId, "sub1", "1")
setupAlert(reg2.AlertId, "sub2", "1")
setupAlert(reg3WithoutSubscription.AlertId, "sub-without-subscription", "1")
// Setup Subscriptions
setupSubscription := func(name string, revision string, component string) {
query := `
INSERT INTO Subscriptions (name, revision, bug_component)
VALUES ($1, $2, $3)
ON CONFLICT (name, revision)
DO UPDATE SET
name = EXCLUDED.name,
revision = EXCLUDED.revision,
bug_component = EXCLUDED.bug_component`
_, err := store.db.Exec(ctx, query, name, revision, component)
require.NoError(t, err)
}
setupSubscription("sub1", "1", component1)
setupSubscription("sub2", "1", component2)
// 2. Test Cases
tests := []struct {
name string
regressionIDs []string
expectedRegressionIDs []string
expectedAlertIDs []int64
expectedBugComponents []string
expectError bool
expectedErrorContains string
}{
{
name: "happy path - get multiple subscriptions",
regressionIDs: []string{reg1.Id, reg2.Id},
expectedRegressionIDs: []string{reg1.Id, reg2.Id},
expectedAlertIDs: []int64{reg1.AlertId, reg2.AlertId},
expectedBugComponents: []string{component1, component2},
},
{
name: "single regression",
regressionIDs: []string{reg1.Id},
expectedRegressionIDs: []string{reg1.Id},
expectedAlertIDs: []int64{reg1.AlertId},
expectedBugComponents: []string{component1},
},
{
name: "regression without subscription",
regressionIDs: []string{reg3WithoutSubscription.Id},
expectedRegressionIDs: nil,
expectedAlertIDs: nil,
expectedBugComponents: nil,
},
{
name: "non-existent regression ID",
regressionIDs: []string{"non-existent-id"},
expectedRegressionIDs: nil,
expectedAlertIDs: nil,
expectedBugComponents: nil,
},
{
name: "empty regression IDs",
regressionIDs: []string{},
expectedRegressionIDs: nil,
expectedAlertIDs: nil,
expectedBugComponents: nil,
},
{
name: "mixed existent and non-existent",
regressionIDs: []string{reg1.Id, "non-existent-id"},
expectedRegressionIDs: []string{reg1.Id},
expectedAlertIDs: []int64{reg1.AlertId},
expectedBugComponents: []string{component1},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
regressionIDs, alertIDs, subs, err := store.GetSubscriptionsForRegressions(ctx, tc.regressionIDs)
if tc.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedErrorContains)
return
}
require.NoError(t, err)
assert.ElementsMatch(t, tc.expectedRegressionIDs, regressionIDs)
assert.ElementsMatch(t, tc.expectedAlertIDs, alertIDs)
if len(tc.expectedBugComponents) > 0 {
bugComponents := []string{}
for _, sub := range subs {
bugComponents = append(bugComponents, sub.BugComponent)
}
assert.ElementsMatch(t, tc.expectedBugComponents, bugComponents)
} else {
assert.Empty(t, subs)
}
})
}
}
func TestGetBugIdsForRegressions(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
store := setupStore(t, alertsProvider)
ctx := context.Background()
// Test case 1: No bugs.
t.Run("No bugs", func(t *testing.T) {
r := generateAndStoreNewRegression(ctx, t, store)
regressions, err := store.GetBugIdsForRegressions(ctx, []*regression.Regression{r})
require.NoError(t, err)
require.Len(t, regressions, 1)
assert.Empty(t, regressions[0].Bugs)
assert.True(t, regressions[0].AllBugsFetched)
})
// Test case 2: Manual bug only.
t.Run("Manual bug only", func(t *testing.T) {
r := generateNewRegression()
manualBug := regression.RegressionBug{BugId: "12345", Type: regression.ManualTriage}
r.Bugs = []regression.RegressionBug{manualBug}
_, err := store.WriteRegression(ctx, r, nil)
require.NoError(t, err)
// Get the regression from DB to make sure manual bug is loaded.
regressionsFromDB, err := store.GetByIDs(ctx, []string{r.Id})
require.NoError(t, err)
require.Len(t, regressionsFromDB, 1)
regressions, err := store.GetBugIdsForRegressions(ctx, regressionsFromDB)
require.NoError(t, err)
require.Len(t, regressions, 1)
assert.ElementsMatch(t, []regression.RegressionBug{manualBug}, regressions[0].Bugs)
assert.True(t, regressions[0].AllBugsFetched)
})
// Test case 3: Auto-triaged bug.
t.Run("Auto-triaged bug", func(t *testing.T) {
r := generateAndStoreNewRegression(ctx, t, store)
agID := uuid.NewString()
reportedIssueID := "123456"
_, err := store.db.Exec(ctx, `
INSERT INTO AnomalyGroups (id, anomaly_ids, common_rev_start, common_rev_end, reported_issue_id)
VALUES ($1, $2, $3, $4, $5)`,
agID, []string{r.Id}, r.PrevCommitNumber, r.CommitNumber, reportedIssueID)
require.NoError(t, err)
regressions, err := store.GetBugIdsForRegressions(ctx, []*regression.Regression{r})
require.NoError(t, err)
require.Len(t, regressions, 1)
expectedBugs := []regression.RegressionBug{
{BugId: reportedIssueID, Type: regression.AutoTriage},
}
assert.ElementsMatch(t, expectedBugs, regressions[0].Bugs)
assert.True(t, regressions[0].AllBugsFetched)
})
// Test case 4: Auto-triaged bug with invalid (start > end) groups.
// We have to filter out groups with start > end revisions as they are incorrect.
t.Run("Auto-triaged bug with invalid groups", func(t *testing.T) {
r := generateAndStoreNewRegression(ctx, t, store)
agID := uuid.NewString()
reportedIssueID := "123456"
_, err := store.db.Exec(ctx, `
INSERT INTO AnomalyGroups (id, anomaly_ids, common_rev_start, common_rev_end, reported_issue_id)
VALUES ($1, $2, $3, $4, $5)`,
agID, []string{r.Id}, r.PrevCommitNumber, r.CommitNumber, reportedIssueID)
require.NoError(t, err)
agID2 := uuid.NewString()
reportedIssueID2 := "67"
_, err = store.db.Exec(ctx, `
INSERT INTO AnomalyGroups (id, anomaly_ids, common_rev_start, common_rev_end, reported_issue_id)
VALUES ($1, $2, $3, $4, $5)`,
agID2, []string{r.Id}, r.CommitNumber+1, r.CommitNumber, reportedIssueID2)
require.NoError(t, err)
regressions, err := store.GetBugIdsForRegressions(ctx, []*regression.Regression{r})
require.NoError(t, err)
require.Len(t, regressions, 1)
expectedBugs := []regression.RegressionBug{
{BugId: reportedIssueID, Type: regression.AutoTriage},
}
assert.ElementsMatch(t, expectedBugs, regressions[0].Bugs)
assert.True(t, regressions[0].AllBugsFetched)
})
// Test case 5: Auto-bisect bug from culprit (group_issue_map).
t.Run("Auto-bisect bug from group_issue_map", func(t *testing.T) {
r := generateAndStoreNewRegression(ctx, t, store)
r2 := generateAndStoreNewRegression(ctx, t, store)
agID := uuid.NewString()
agID2 := uuid.NewString()
culpritID := uuid.NewString()
bisectIssueID := "1234567"
bisectIssueID2 := "12345672"
groupIssueMap := fmt.Sprintf(`{"%s": "%s", "%s": "%s"}`, agID, bisectIssueID, agID2, bisectIssueID2)
_, err := store.db.Exec(ctx, `
INSERT INTO AnomalyGroups (id, anomaly_ids, common_rev_start, common_rev_end)
VALUES ($1, $2, $3, $4)`,
agID, []string{r.Id}, r.PrevCommitNumber, r.CommitNumber)
require.NoError(t, err)
_, err = store.db.Exec(ctx, `
INSERT INTO AnomalyGroups (id, anomaly_ids, common_rev_start, common_rev_end)
VALUES ($1, $2, $3, $4)`,
agID2, []string{r2.Id}, r2.PrevCommitNumber, r2.CommitNumber)
require.NoError(t, err)
_, err = store.db.Exec(ctx, `
INSERT INTO Culprits (id, host, project, ref, revision, anomaly_group_ids, group_issue_map, issue_ids)
VALUES ($1, 'host', 'project', 'ref', 'rev', $2, $3, $4)`,
culpritID, []string{agID, agID2}, groupIssueMap, []string{bisectIssueID, bisectIssueID2})
require.NoError(t, err)
regressions, err := store.GetBugIdsForRegressions(ctx, []*regression.Regression{r})
require.NoError(t, err)
require.Len(t, regressions, 1)
expectedBugs := []regression.RegressionBug{
{BugId: bisectIssueID, Type: regression.AutoBisect},
}
assert.ElementsMatch(t, expectedBugs, regressions[0].Bugs)
})
// Test case 6: All bug types together.
t.Run("All bug types together", func(t *testing.T) {
r := generateNewRegression()
manualBug := regression.RegressionBug{BugId: "123", Type: regression.ManualTriage}
r.Bugs = []regression.RegressionBug{manualBug}
_, err := store.WriteRegression(ctx, r, nil)
require.NoError(t, err)
// Auto-triage bug
agID1 := uuid.NewString()
reportedIssueID := "456"
_, err = store.db.Exec(ctx, `
INSERT INTO AnomalyGroups (id, anomaly_ids, common_rev_start, common_rev_end, reported_issue_id)
VALUES ($1, $2, $3, $4, $5)`,
agID1, []string{r.Id}, r.PrevCommitNumber, r.CommitNumber, reportedIssueID)
require.NoError(t, err)
// Auto-bisect bug
agID2 := uuid.NewString()
culpritID := uuid.NewString()
bisectIssueID := "789"
groupIssueMap := fmt.Sprintf(`{"%s": "%s"}`, agID2, bisectIssueID)
_, err = store.db.Exec(ctx, `
INSERT INTO AnomalyGroups (id, anomaly_ids, common_rev_start, common_rev_end)
VALUES ($1, $2, $3, $4)`,
agID2, []string{r.Id}, r.PrevCommitNumber, r.CommitNumber)
require.NoError(t, err)
_, err = store.db.Exec(ctx, `
INSERT INTO Culprits (id, host, project, ref, revision, anomaly_group_ids, group_issue_map, issue_ids)
VALUES ($1, 'host', 'project', 'ref', 'rev', $2, $3, $4)`,
culpritID, []string{agID2}, groupIssueMap, []string{bisectIssueID})
require.NoError(t, err)
// Fetch the manual bug first.
regressionsFromDB, err := store.GetByIDs(ctx, []string{r.Id})
require.NoError(t, err)
require.Len(t, regressionsFromDB, 1)
regressions, err := store.GetBugIdsForRegressions(ctx, regressionsFromDB)
require.NoError(t, err)
require.Len(t, regressions, 1)
expectedBugs := []regression.RegressionBug{
manualBug,
{BugId: reportedIssueID, Type: regression.AutoTriage},
{BugId: bisectIssueID, Type: regression.AutoBisect},
}
assert.ElementsMatch(t, expectedBugs, regressions[0].Bugs)
})
}
// TestGetIdsByManualTriageBugID tests the GetIdsByManualTriageBugID method.
func TestGetIdsByManualTriageBugID(t *testing.T) {
alertsProvider := alerts_mock.NewConfigProvider(t)
store := setupStore(t, alertsProvider)
ctx := context.Background()
// 1. Setup: Insert some regressions and assign manual triage bug IDs.
r1 := generateAndStoreNewRegression(ctx, t, store)
r2 := generateAndStoreNewRegression(ctx, t, store)
r3 := generateAndStoreNewRegression(ctx, t, store)
bugID1 := 10001
bugID2 := 10002
err := store.SetBugID(ctx, []string{r1.Id, r2.Id}, bugID1)
require.NoError(t, err)
err = store.SetBugID(ctx, []string{r3.Id}, bugID2)
require.NoError(t, err)
tests := []struct {
name string
bugID int
expectedCount int
expectedIDs []string
}{
{
name: "find two regressions with bugID1",
bugID: bugID1,
expectedCount: 2,
expectedIDs: []string{r1.Id, r2.Id},
},
{
name: "find one regression with bugID2",
bugID: bugID2,
expectedCount: 1,
expectedIDs: []string{r3.Id},
},
{
name: "no regressions found for non-existent bug ID",
bugID: 99999,
expectedCount: 0,
expectedIDs: []string{},
},
{
name: "zero bug ID returns nothing",
bugID: 0,
expectedCount: 0,
expectedIDs: []string{},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
regIds, err := store.GetIdsByManualTriageBugID(ctx, tc.bugID)
require.NoError(t, err)
assert.Len(t, regIds, tc.expectedCount)
actualIDs := []string{}
for _, regId := range regIds {
actualIDs = append(actualIDs, regId)
}
assert.ElementsMatch(t, tc.expectedIDs, actualIDs)
})
}
}