blob: 5810ceb704b01c7c34b6c8d98a7356ead39a3a84 [file] [log] [blame]
package search
import (
"context"
"errors"
"fmt"
"sync/atomic"
"testing"
"time"
ttlcache "github.com/patrickmn/go-cache"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.skia.org/infra/go/paramtools"
"go.skia.org/infra/go/testutils"
"go.skia.org/infra/go/testutils/unittest"
mock_clstore "go.skia.org/infra/golden/go/clstore/mocks"
"go.skia.org/infra/golden/go/code_review"
"go.skia.org/infra/golden/go/comment"
mock_comment "go.skia.org/infra/golden/go/comment/mocks"
"go.skia.org/infra/golden/go/comment/trace"
"go.skia.org/infra/golden/go/diff"
mock_diffstore "go.skia.org/infra/golden/go/diffstore/mocks"
"go.skia.org/infra/golden/go/digest_counter"
"go.skia.org/infra/golden/go/expectations"
mock_expectations "go.skia.org/infra/golden/go/expectations/mocks"
"go.skia.org/infra/golden/go/indexer"
mock_index "go.skia.org/infra/golden/go/indexer/mocks"
"go.skia.org/infra/golden/go/paramsets"
"go.skia.org/infra/golden/go/search/common"
"go.skia.org/infra/golden/go/search/frontend"
"go.skia.org/infra/golden/go/search/query"
data "go.skia.org/infra/golden/go/testutils/data_three_devices"
"go.skia.org/infra/golden/go/tiling"
"go.skia.org/infra/golden/go/tjstore"
mock_tjstore "go.skia.org/infra/golden/go/tjstore/mocks"
"go.skia.org/infra/golden/go/types"
)
// TODO(kjlubick) Add tests for:
// - When a CL doesn't exist or the CL has not patchsets, patchset doesn't exist,
// or otherwise no results.
// - Use ignore matcher
// - When a CL specifies a PS
// - IncludeMaster=true
// - UnavailableDigests is not empty
// - DiffSever/RefDiffer error
// TestSearch_UntriagedDigestsAtHead_Success searches over the three_devices
// test data for untriaged images at head, essentially the default search.
// We expect to get two untriaged digests, with their closest positive and
// negative images (if any).
func TestSearch_UntriagedDigestsAtHead_Success(t *testing.T) {
unittest.SmallTest(t)
mds := makeDiffStoreWithNoFailures()
addDiffData(mds, data.AlphaUntriagedDigest, data.AlphaPositiveDigest, makeSmallDiffMetric())
addDiffData(mds, data.AlphaUntriagedDigest, data.AlphaNegativeDigest, makeBigDiffMetric())
addDiffData(mds, data.BetaUntriagedDigest, data.BetaPositiveDigest, makeBigDiffMetric())
// BetaUntriagedDigest has no negative images to compare against.
s := New(mds, makeThreeDevicesExpectationStore(), nil, makeThreeDevicesIndexer(), nil, nil, emptyCommentStore(), everythingPublic)
q := &query.Search{
ChangeListID: "",
Unt: true,
Head: true,
Metric: diff.CombinedMetric,
FRGBAMin: 0,
FRGBAMax: 255,
FDiffMax: -1,
Sort: query.SortAscending,
}
resp, err := s.Search(context.Background(), q)
require.NoError(t, err)
require.NotNil(t, resp)
assert.Equal(t, &frontend.SearchResponse{
Commits: data.MakeTestCommits(),
Offset: 0,
Size: 2,
Digests: []*frontend.SRDigest{
// AlphaTest comes first because we are sorting by ascending
// "combined" metric, and AlphaTest's closest match is the
// small diff metric, whereas BetaTest's only match is the
// big diff metric.
{
Test: data.AlphaTest,
Digest: data.AlphaUntriagedDigest,
Status: "untriaged",
ParamSet: map[string][]string{
"device": {data.BullheadDevice},
types.PrimaryKeyField: {string(data.AlphaTest)},
types.CorpusField: {"gm"},
},
Traces: &frontend.TraceGroup{
TileSize: 3, // 3 commits in tile
TotalDigests: 2,
Traces: []frontend.Trace{
{
Data: []int{1, 1, 0},
ID: data.BullheadAlphaTraceID,
Params: map[string]string{
"device": data.BullheadDevice,
types.PrimaryKeyField: string(data.AlphaTest),
types.CorpusField: "gm",
},
},
},
Digests: []frontend.DigestStatus{
{
Digest: data.AlphaUntriagedDigest,
Status: "untriaged",
},
{
Digest: data.AlphaNegativeDigest,
Status: "negative",
},
},
},
ClosestRef: common.PositiveRef,
RefDiffs: map[common.RefClosest]*frontend.SRDiffDigest{
common.PositiveRef: {
DiffMetrics: makeSmallDiffMetric(),
Digest: data.AlphaPositiveDigest,
Status: "positive",
ParamSet: map[string][]string{
"device": {data.AnglerDevice, data.CrosshatchDevice},
types.PrimaryKeyField: {string(data.AlphaTest)},
types.CorpusField: {"gm"},
},
OccurrencesInTile: 2,
},
common.NegativeRef: {
DiffMetrics: makeBigDiffMetric(),
Digest: data.AlphaNegativeDigest,
Status: "negative",
ParamSet: map[string][]string{
"device": {data.AnglerDevice, data.BullheadDevice, data.CrosshatchDevice},
types.PrimaryKeyField: {string(data.AlphaTest)},
types.CorpusField: {"gm"},
},
OccurrencesInTile: 6,
},
},
},
{
Test: data.BetaTest,
Digest: data.BetaUntriagedDigest,
Status: "untriaged",
ParamSet: map[string][]string{
"device": {data.CrosshatchDevice},
types.PrimaryKeyField: {string(data.BetaTest)},
types.CorpusField: {"gm"},
},
Traces: &frontend.TraceGroup{
TileSize: 3,
TotalDigests: 1,
Traces: []frontend.Trace{
{
Data: []int{0, missingDigestIndex, missingDigestIndex},
ID: data.CrosshatchBetaTraceID,
Params: map[string]string{
"device": data.CrosshatchDevice,
types.PrimaryKeyField: string(data.BetaTest),
types.CorpusField: "gm",
},
},
},
Digests: []frontend.DigestStatus{
{
Digest: data.BetaUntriagedDigest,
Status: "untriaged",
},
},
},
ClosestRef: common.PositiveRef,
RefDiffs: map[common.RefClosest]*frontend.SRDiffDigest{
common.PositiveRef: {
DiffMetrics: makeBigDiffMetric(),
Digest: data.BetaPositiveDigest,
Status: "positive",
ParamSet: map[string][]string{
"device": {data.AnglerDevice, data.BullheadDevice},
types.PrimaryKeyField: {string(data.BetaTest)},
types.CorpusField: {"gm"},
},
OccurrencesInTile: 6,
},
common.NegativeRef: nil,
},
},
},
}, resp)
}
// TestSearch_UntriagedWithLimitAndOffset_LimitAndOffsetRespected makes a search setting the limit
// to be less than the total number of results (2) and making sure the results respect both the
// limit and offset inputs.
func TestSearch_UntriagedWithLimitAndOffset_LimitAndOffsetRespected(t *testing.T) {
unittest.SmallTest(t)
mds := makeDiffStoreWithNoFailures()
addDiffData(mds, data.AlphaUntriagedDigest, data.AlphaPositiveDigest, makeSmallDiffMetric())
addDiffData(mds, data.AlphaUntriagedDigest, data.AlphaNegativeDigest, makeBigDiffMetric())
addDiffData(mds, data.BetaUntriagedDigest, data.BetaPositiveDigest, makeBigDiffMetric())
// BetaUntriagedDigest has no negative images to compare against.
s := New(mds, makeThreeDevicesExpectationStore(), nil, makeThreeDevicesIndexer(), nil, nil, emptyCommentStore(), everythingPublic)
q := &query.Search{
ChangeListID: "",
Unt: true,
Offset: 0,
Limit: 1,
Metric: diff.CombinedMetric,
FRGBAMin: 0,
FRGBAMax: 255,
FDiffMax: -1,
Sort: query.SortAscending,
}
resp, err := s.Search(context.Background(), q)
require.NoError(t, err)
require.NotNil(t, resp)
assert.Len(t, resp.Digests, 1)
assert.Equal(t, resp.Offset, 0)
assert.Equal(t, resp.Size, 2)
// This checks that the returned result is the first one of the results we expect.
assert.Equal(t, data.AlphaUntriagedDigest, resp.Digests[0].Digest)
q.Offset = 1
q.Limit = 100 // There's only 2 results in the total search, i.e. one remaining, so set this
// high to make sure nothing breaks.
resp, err = s.Search(context.Background(), q)
require.NoError(t, err)
require.NotNil(t, resp)
assert.Len(t, resp.Digests, 1)
assert.Equal(t, resp.Offset, 1)
assert.Equal(t, resp.Size, 2)
// This checks that the returned result is the second one of the results we expect.
assert.Equal(t, data.BetaUntriagedDigest, resp.Digests[0].Digest)
}
// TestSearchThreeDevicesQueries searches over the three_devices test data using a variety
// of queries. It only spot-checks the returned data (e.g. things are in the right order); other
// tests should do a more thorough check of the return values.
func TestSearchThreeDevicesQueries(t *testing.T) {
unittest.SmallTest(t)
mds := makeDiffStoreWithNoFailures()
addDiffData(mds, data.AlphaUntriagedDigest, data.AlphaPositiveDigest, makeSmallDiffMetric())
addDiffData(mds, data.AlphaUntriagedDigest, data.AlphaNegativeDigest, makeBigDiffMetric())
addDiffData(mds, data.BetaUntriagedDigest, data.BetaPositiveDigest, makeBigDiffMetric())
// BetaUntriagedDigest has no negative images to compare against.
s := New(mds, makeThreeDevicesExpectationStore(), nil, makeThreeDevicesIndexer(), nil, nil, emptyCommentStore(), everythingPublic)
// spotCheck is the subset of data we assert against.
type spotCheck struct {
test types.TestName
digest types.Digest
labelStr string
closestPositive types.Digest
closestNegative types.Digest
}
test := func(name string, input *query.Search, expectedOutputs []spotCheck) {
t.Run(name, func(t *testing.T) {
resp, err := s.Search(context.Background(), input)
require.NoError(t, err)
require.NotNil(t, resp)
require.Len(t, resp.Digests, len(expectedOutputs))
for i, actualDigest := range resp.Digests {
expected := expectedOutputs[i]
assert.Equal(t, expected.test, actualDigest.Test)
assert.Equal(t, expected.digest, actualDigest.Digest)
assert.Equal(t, expected.labelStr, actualDigest.Status)
if expected.closestPositive == "" {
assert.Nil(t, actualDigest.RefDiffs[common.PositiveRef])
} else {
cp := actualDigest.RefDiffs[common.PositiveRef]
require.NotNil(t, cp)
assert.Equal(t, expected.closestPositive, cp.Digest)
}
if expected.closestNegative == "" {
assert.Nil(t, actualDigest.RefDiffs[common.NegativeRef])
} else {
cp := actualDigest.RefDiffs[common.NegativeRef]
require.NotNil(t, cp)
assert.Equal(t, expected.closestNegative, cp.Digest)
}
}
})
}
test("default query, but in reverse", &query.Search{
Unt: true,
Head: true,
Metric: diff.CombinedMetric,
FRGBAMin: 0,
FRGBAMax: 255,
FDiffMax: -1,
Sort: query.SortDescending,
}, []spotCheck{
{
test: data.BetaTest,
digest: data.BetaUntriagedDigest,
labelStr: "untriaged",
closestPositive: data.BetaPositiveDigest,
closestNegative: "",
},
{
test: data.AlphaTest,
digest: data.AlphaUntriagedDigest,
labelStr: "untriaged",
closestPositive: data.AlphaPositiveDigest,
closestNegative: data.AlphaNegativeDigest,
},
})
test("the closest RGBA diff should be at least 50 units away", &query.Search{
Unt: true,
Head: true,
Metric: diff.CombinedMetric,
FRGBAMin: 50,
FRGBAMax: 255,
FDiffMax: -1,
Sort: query.SortDescending,
}, []spotCheck{
{
test: data.BetaTest,
digest: data.BetaUntriagedDigest,
labelStr: "untriaged",
closestPositive: data.BetaPositiveDigest,
closestNegative: "",
},
})
// note: this matches only the makeSmallDiffMetric
test("the closest RGBA diff should be no more than 50 units away", &query.Search{
Unt: true,
Head: true,
Metric: diff.CombinedMetric,
FRGBAMin: 0,
FRGBAMax: 50,
FDiffMax: -1,
Sort: query.SortDescending,
}, []spotCheck{
{
test: data.AlphaTest,
digest: data.AlphaUntriagedDigest,
labelStr: "untriaged",
closestPositive: data.AlphaPositiveDigest,
closestNegative: data.AlphaNegativeDigest,
},
})
test("combined diff metric less than 1", &query.Search{
Unt: true,
Head: true,
Metric: diff.CombinedMetric,
FRGBAMin: 0,
FRGBAMax: 255,
FDiffMax: 1,
Sort: query.SortDescending,
}, []spotCheck{
{
test: data.AlphaTest,
digest: data.AlphaUntriagedDigest,
labelStr: "untriaged",
closestPositive: data.AlphaPositiveDigest,
closestNegative: data.AlphaNegativeDigest,
},
})
test("percent diff metric less than 1", &query.Search{
Unt: true,
Head: true,
Metric: diff.PercentMetric,
FRGBAMin: 0,
FRGBAMax: 255,
FDiffMax: 1,
Sort: query.SortDescending,
}, []spotCheck{
{
test: data.AlphaTest,
digest: data.AlphaUntriagedDigest,
labelStr: "untriaged",
closestPositive: data.AlphaPositiveDigest,
closestNegative: data.AlphaNegativeDigest,
},
})
test("Fewer than 10 different pixels", &query.Search{
Unt: true,
Head: true,
Metric: diff.PixelMetric,
FRGBAMin: 0,
FRGBAMax: 255,
FDiffMax: 10,
Sort: query.SortDescending,
}, []spotCheck{
{
test: data.AlphaTest,
digest: data.AlphaUntriagedDigest,
labelStr: "untriaged",
closestPositive: data.AlphaPositiveDigest,
closestNegative: data.AlphaNegativeDigest,
},
})
test("Nothing has fewer than 10 different pixels and min RGBA diff >50", &query.Search{
Unt: true,
Head: true,
Metric: diff.PixelMetric,
FRGBAMin: 50,
FRGBAMax: 255,
FDiffMax: 10,
Sort: query.SortDescending,
}, nil)
test("default query, only those with a reference diff (all of them)", &query.Search{
Unt: true,
Head: true,
FRef: true,
Metric: diff.CombinedMetric,
FRGBAMin: 0,
FRGBAMax: 255,
FDiffMax: -1,
Sort: query.SortAscending,
}, []spotCheck{
{
test: data.AlphaTest,
digest: data.AlphaUntriagedDigest,
labelStr: "untriaged",
closestPositive: data.AlphaPositiveDigest,
closestNegative: data.AlphaNegativeDigest,
},
{
test: data.BetaTest,
digest: data.BetaUntriagedDigest,
labelStr: "untriaged",
closestPositive: data.BetaPositiveDigest,
closestNegative: "",
},
})
test("starting at the second commit, we only see alpha's untriaged commit at head", &query.Search{
Unt: true,
Head: true,
FCommitBegin: data.MakeTestCommits()[1].Hash,
Metric: diff.CombinedMetric,
FRGBAMin: 0,
FRGBAMax: 255,
FDiffMax: -1,
Sort: query.SortAscending,
}, []spotCheck{
{
test: data.AlphaTest,
digest: data.AlphaUntriagedDigest,
labelStr: "untriaged",
closestPositive: data.AlphaPositiveDigest,
closestNegative: data.AlphaNegativeDigest,
},
})
test("starting at the second commit, we see both if we ignore the head restriction", &query.Search{
Unt: true,
Head: false,
FCommitBegin: data.MakeTestCommits()[1].Hash,
Metric: diff.CombinedMetric,
FRGBAMin: 0,
FRGBAMax: 255,
FDiffMax: -1,
Sort: query.SortAscending,
}, []spotCheck{
{
test: data.AlphaTest,
digest: data.AlphaUntriagedDigest,
labelStr: "untriaged",
closestPositive: data.AlphaPositiveDigest,
closestNegative: data.AlphaNegativeDigest,
},
{
test: data.BetaTest,
digest: data.BetaUntriagedDigest,
labelStr: "untriaged",
closestPositive: data.BetaPositiveDigest,
closestNegative: "",
},
})
test("stopping at the second commit, we only see beta's untriaged", &query.Search{
Unt: true,
Head: true,
FCommitEnd: data.MakeTestCommits()[1].Hash,
Metric: diff.CombinedMetric,
FRGBAMin: 0,
FRGBAMax: 255,
FDiffMax: -1,
Sort: query.SortAscending,
}, []spotCheck{
{
test: data.BetaTest,
digest: data.BetaUntriagedDigest,
labelStr: "untriaged",
closestPositive: data.BetaPositiveDigest,
closestNegative: "",
},
})
test("query matches nothing", &query.Search{
Unt: true,
Head: true,
TraceValues: map[string][]string{
"blubber": {"nothing"},
},
Metric: diff.CombinedMetric,
FRGBAMin: 0,
FRGBAMax: 255,
FDiffMax: -1,
Sort: query.SortDescending,
}, []spotCheck{})
}
// TestSearch_ThreeDevicesCorpusWithComments_CommentsInResults ensures that search results contain
// comments when it matches the traces.
func TestSearch_ThreeDevicesCorpusWithComments_CommentsInResults(t *testing.T) {
unittest.SmallTest(t)
bullheadComment := trace.Comment{
ID: "1",
CreatedBy: "zulu@example.com",
UpdatedBy: "zulu@example.com",
CreatedTS: time.Date(2020, time.February, 19, 18, 17, 16, 0, time.UTC),
UpdatedTS: time.Date(2020, time.February, 19, 18, 17, 16, 0, time.UTC),
Comment: "All bullhead devices draw upside down",
QueryToMatch: paramtools.ParamSet{
"device": []string{data.BullheadDevice},
},
}
alphaTestComment := trace.Comment{
ID: "2",
CreatedBy: "yankee@example.com",
UpdatedBy: "xray@example.com",
CreatedTS: time.Date(2020, time.February, 2, 18, 17, 16, 0, time.UTC),
UpdatedTS: time.Date(2020, time.February, 20, 18, 17, 16, 0, time.UTC),
Comment: "Watch pixel 0,4 to make sure it's not purple",
QueryToMatch: paramtools.ParamSet{
types.PrimaryKeyField: []string{string(data.AlphaTest)},
},
}
betaTestBullheadComment := trace.Comment{
ID: "4",
CreatedBy: "victor@example.com",
UpdatedBy: "victor@example.com",
CreatedTS: time.Date(2020, time.February, 22, 18, 17, 16, 0, time.UTC),
UpdatedTS: time.Date(2020, time.February, 22, 18, 17, 16, 0, time.UTC),
Comment: "Being upside down, this test should be ABGR instead of RGBA",
QueryToMatch: paramtools.ParamSet{
"device": []string{data.BullheadDevice},
types.PrimaryKeyField: []string{string(data.BetaTest)},
},
}
commentAppliesToNothing := trace.Comment{
ID: "3",
CreatedBy: "uniform@example.com",
UpdatedBy: "uniform@example.com",
CreatedTS: time.Date(2020, time.February, 26, 26, 26, 26, 0, time.UTC),
UpdatedTS: time.Date(2020, time.February, 26, 26, 26, 26, 0, time.UTC),
Comment: "On Wednesdays, this device draws pink",
QueryToMatch: paramtools.ParamSet{
"device": []string{"This device does not exist"},
},
}
mcs := &mock_comment.Store{}
// Return these in an arbitrary, unsorted order
mcs.On("ListComments", testutils.AnyContext).Return([]trace.Comment{commentAppliesToNothing, alphaTestComment, betaTestBullheadComment, bullheadComment}, nil)
s := New(makeStubDiffStore(), makeThreeDevicesExpectationStore(), nil, makeThreeDevicesIndexer(), nil, nil, mcs, everythingPublic)
q := &query.Search{
// Set all to true so all 6 traces show up in the final results.
Unt: true,
Pos: true,
Neg: true,
Head: true,
Metric: diff.CombinedMetric,
FRGBAMin: 0,
FRGBAMax: 255,
FDiffMax: -1,
Sort: query.SortAscending,
}
resp, err := s.Search(context.Background(), q)
require.NoError(t, err)
require.NotNil(t, resp)
// There are 4 unique digests at HEAD on the three_devices corpus. Do a quick smoke test to make
// sure we have one search result for each of them.
require.Len(t, resp.Digests, 4)
f := frontend.ToTraceComment
// This should be sorted by UpdatedTS.
assert.Equal(t, []frontend.TraceComment{
f(bullheadComment), f(alphaTestComment), f(betaTestBullheadComment), f(commentAppliesToNothing),
}, resp.TraceComments)
// These numbers are indices into the resp.TraceComments. The nil entries are expected to have
// no comments that match them.
expectedComments := map[tiling.TraceID][]int{
data.AnglerAlphaTraceID: {1},
data.AnglerBetaTraceID: nil,
data.BullheadAlphaTraceID: {0, 1},
data.BullheadBetaTraceID: {0, 2},
data.CrosshatchAlphaTraceID: {1},
data.CrosshatchBetaTraceID: nil,
}
// We only check that the traces have their associated comments. We rely on the other tests
// to make sure the other fields are correct.
traceCount := 0
for _, r := range resp.Digests {
for _, tr := range r.Traces.Traces {
traceCount++
assert.Equal(t, expectedComments[tr.ID], tr.CommentIndices, "trace id %q under digest", tr.ID, r.Digest)
}
}
assert.Equal(t, 6, traceCount, "Not all traces were in the final result")
}
// TestSearch_ChangeListResults_ChangeListIndexMiss_Success covers the case
// where two tryjobs have been run on a given CL and PS, one on the
// angler bot and one on the bullhead bot. The master branch
// looks like in the ThreeDevices data set. The outputs produced are
// Test | Device | Digest
// ----------------------
// Alpha | Angler | data.AlphaPositiveDigest
// Alpha | Bullhead | data.AlphaUntriagedDigest
// Beta | Angler | data.BetaPositiveDigest
// Beta | Bullhead | BetaBrandNewDigest
//
// The user has triaged the data.AlphaUntriagedDigest as positive
// but BetaBrandNewDigest remains untriaged.
// With this setup, we do a default query (don't show master,
// only untriaged digests) and expect to see only an entry about
// BetaBrandNewDigest.
func TestSearch_ChangeListResults_ChangeListIndexMiss_Success(t *testing.T) {
unittest.SmallTest(t)
const clID = "1234"
const crs = "gerrit"
const AlphaNowGoodDigest = data.AlphaUntriagedDigest
const BetaBrandNewDigest = types.Digest("be7a03256511bec3a7453c3186bb2e07")
mcls := &mock_clstore.Store{}
mtjs := &mock_tjstore.Store{}
defer mcls.AssertExpectations(t)
defer mtjs.AssertExpectations(t)
mes := makeThreeDevicesExpectationStore()
var ie expectations.Expectations
ie.Set(data.AlphaTest, AlphaNowGoodDigest, expectations.Positive)
addChangeListExpectations(mes, crs, clID, &ie)
mcls.On("GetPatchSets", testutils.AnyContext, clID).Return([]code_review.PatchSet{
{
SystemID: "first_one",
ChangeListID: clID,
Order: 1,
// All the rest are ignored
},
{
SystemID: "fourth_one",
ChangeListID: clID,
Order: 4,
// All the rest are ignored
},
}, nil).Once() // this should be cached after fetch, as it could be expensive to retrieve.
mcls.On("System").Return(crs)
expectedID := tjstore.CombinedPSID{
CL: clID,
CRS: crs,
PS: "fourth_one", // we didn't specify a PS, so it goes with the most recent
}
anglerGroup := map[string]string{
"device": data.AnglerDevice,
}
bullheadGroup := map[string]string{
"device": data.BullheadDevice,
}
options := map[string]string{
"ext": "png",
}
mtjs.On("GetResults", testutils.AnyContext, expectedID).Return([]tjstore.TryJobResult{
{
GroupParams: anglerGroup,
Options: options,
Digest: data.AlphaPositiveDigest,
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.AlphaTest),
types.CorpusField: "gm",
},
},
{
GroupParams: bullheadGroup,
Options: options,
Digest: AlphaNowGoodDigest,
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.AlphaTest),
types.CorpusField: "gm",
},
},
{
GroupParams: anglerGroup,
Options: options,
Digest: data.BetaPositiveDigest,
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.BetaTest),
types.CorpusField: "gm",
},
},
{
GroupParams: bullheadGroup,
Options: options,
Digest: BetaBrandNewDigest,
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.BetaTest),
types.CorpusField: "gm",
},
},
}, nil).Once() // this should be cached after fetch, as it could be expensive to retrieve.
mds := makeDiffStoreWithNoFailures()
addDiffData(mds, BetaBrandNewDigest, data.BetaPositiveDigest, makeSmallDiffMetric())
s := New(mds, mes, nil, makeThreeDevicesIndexer(), mcls, mtjs, nil, everythingPublic)
q := &query.Search{
ChangeListID: clID,
NewCLStore: true,
IncludeMaster: false,
Unt: true,
Head: true,
Metric: diff.CombinedMetric,
FRGBAMin: 0,
FRGBAMax: 255,
FDiffMax: -1,
Sort: query.SortAscending,
}
resp, err := s.Search(context.Background(), q)
require.NoError(t, err)
require.NotNil(t, resp)
// make sure the group maps were not mutated.
assert.Len(t, anglerGroup, 1)
assert.Len(t, bullheadGroup, 1)
assert.Len(t, options, 1)
assert.Equal(t, &frontend.SearchResponse{
Commits: data.MakeTestCommits(),
Offset: 0,
Size: 1,
Digests: []*frontend.SRDigest{
{
Test: data.BetaTest,
Digest: BetaBrandNewDigest,
Status: "untriaged",
ParamSet: map[string][]string{
"device": {data.BullheadDevice},
types.PrimaryKeyField: {string(data.BetaTest)},
types.CorpusField: {"gm"},
"ext": {"png"},
},
Traces: &frontend.TraceGroup{
TileSize: 3,
Traces: []frontend.Trace{},
Digests: []frontend.DigestStatus{
{
Digest: BetaBrandNewDigest,
Status: "untriaged",
},
},
},
ClosestRef: common.PositiveRef,
RefDiffs: map[common.RefClosest]*frontend.SRDiffDigest{
common.PositiveRef: {
DiffMetrics: makeSmallDiffMetric(),
Digest: data.BetaPositiveDigest,
Status: "positive",
ParamSet: map[string][]string{
"device": {data.AnglerDevice, data.BullheadDevice},
types.PrimaryKeyField: {string(data.BetaTest)},
types.CorpusField: {"gm"},
// Note: the data from three_devices lacks an "ext" entry, so
// we don't see one here
},
OccurrencesInTile: 6,
},
common.NegativeRef: nil,
},
},
},
}, resp)
// Validate that we cache the .*Store values in two quick responses.
_, err = s.Search(context.Background(), q)
require.NoError(t, err)
}
// TestSearchImpl_ExtractChangeListDigests_CacheHit_Success tests the case where the ChangeList
// index can be used (because we are only searching for untriaged data) and makes sure the results
// are used as if they were freshly queried from firestore.
func TestSearchImpl_ExtractChangeListDigests_CacheHit_Success(t *testing.T) {
unittest.SmallTest(t)
const clID = "1234"
const psID = "the_patchset"
const crs = "gerrit"
combinedID := tjstore.CombinedPSID{CRS: crs, CL: clID, PS: psID}
mi := &mock_index.IndexSource{}
mis := &mock_index.IndexSearcher{}
mcs := &mock_clstore.Store{}
mcs.On("GetPatchSets", testutils.AnyContext, clID).Return([]code_review.PatchSet{
{
SystemID: psID,
ChangeListID: clID,
Order: 1,
// other fields are ignored
},
}, nil)
mcs.On("System").Return(crs)
anglerGroup := map[string]string{
"device": data.AnglerDevice,
}
bullheadGroup := map[string]string{
"device": data.BullheadDevice,
}
options := map[string]string{
"ext": "png",
}
mi.On("GetIndexForCL", crs, clID).Return(&indexer.ChangeListIndex{
UntriagedResultsProduced: map[tjstore.CombinedPSID][]tjstore.TryJobResult{
combinedID: {
{
GroupParams: anglerGroup,
Options: options,
Digest: data.AlphaUntriagedDigest,
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.AlphaTest),
types.CorpusField: "gm",
},
},
{
GroupParams: bullheadGroup,
Options: options,
Digest: data.BetaUntriagedDigest,
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.BetaTest),
types.CorpusField: "gm",
},
},
},
},
})
// No ignore rules
mis.On("GetIgnoreMatcher").Return(paramtools.ParamMatcher{})
s := SearchImpl{
indexSource: mi,
changeListStore: mcs,
tryJobStore: nil, // we should not actually hit the TryJobStore, because the cache was used.
storeCache: ttlcache.New(0, 0),
}
q := &query.Search{
ChangeListID: clID,
NewCLStore: true,
IncludeMaster: false,
Unt: true,
}
alphaSeenCount := int32(0)
betaSeenCount := int32(0)
testAddFn := func(test types.TestName, digest types.Digest, _ paramtools.Params) {
if test == data.AlphaTest && digest == data.AlphaUntriagedDigest {
atomic.AddInt32(&alphaSeenCount, 1)
} else if test == data.BetaTest && digest == data.BetaUntriagedDigest {
atomic.AddInt32(&betaSeenCount, 1)
} else {
assert.Failf(t, "unrecognized input", "%s %s", test, digest)
}
}
err := s.extractChangeListDigests(context.Background(), q, mis, expectations.EmptyClassifier(), testAddFn)
require.NoError(t, err)
assert.Equal(t, int32(1), alphaSeenCount)
assert.Equal(t, int32(1), betaSeenCount)
}
func TestDigestDetailsThreeDevicesSunnyDay(t *testing.T) {
unittest.SmallTest(t)
const digestWeWantDetailsAbout = data.AlphaPositiveDigest
const testWeWantDetailsAbout = data.AlphaTest
mds := makeDiffStoreWithNoFailures()
// Note: If a digest is compared to itself, it is removed from the return value, so we use nil.
addDiffData(mds, digestWeWantDetailsAbout, data.AlphaPositiveDigest, nil)
addDiffData(mds, digestWeWantDetailsAbout, data.AlphaNegativeDigest, makeBigDiffMetric())
s := New(mds, makeThreeDevicesExpectationStore(), nil, makeThreeDevicesIndexer(), nil, nil, emptyCommentStore(), everythingPublic)
details, err := s.GetDigestDetails(context.Background(), testWeWantDetailsAbout, digestWeWantDetailsAbout, "", "")
require.NoError(t, err)
assert.Equal(t, &frontend.DigestDetails{
Commits: data.MakeTestCommits(),
Digest: &frontend.SRDigest{
Test: testWeWantDetailsAbout,
Digest: digestWeWantDetailsAbout,
Status: "positive",
TriageHistory: []frontend.TriageHistory{
{
User: userWhoTriaged,
TS: alphaPositiveTriageTS,
},
},
ParamSet: map[string][]string{
"device": {data.AnglerDevice, data.CrosshatchDevice},
types.PrimaryKeyField: {string(data.AlphaTest)},
types.CorpusField: {"gm"},
},
Traces: &frontend.TraceGroup{
TileSize: 3, // 3 commits in tile
TotalDigests: 2,
Traces: []frontend.Trace{ // the digest we care about appears in two traces
{
Data: []int{1, 1, 0},
ID: data.AnglerAlphaTraceID,
Params: map[string]string{
"device": data.AnglerDevice,
types.PrimaryKeyField: string(data.AlphaTest),
types.CorpusField: "gm",
},
},
{
Data: []int{1, 1, 0},
ID: data.CrosshatchAlphaTraceID,
Params: map[string]string{
"device": data.CrosshatchDevice,
types.PrimaryKeyField: string(data.AlphaTest),
types.CorpusField: "gm",
},
},
},
Digests: []frontend.DigestStatus{
{
Digest: data.AlphaPositiveDigest,
Status: "positive",
},
{
Digest: data.AlphaNegativeDigest,
Status: "negative",
},
},
},
ClosestRef: common.NegativeRef,
RefDiffs: map[common.RefClosest]*frontend.SRDiffDigest{
common.PositiveRef: nil,
common.NegativeRef: {
DiffMetrics: makeBigDiffMetric(),
Digest: data.AlphaNegativeDigest,
Status: "negative",
ParamSet: map[string][]string{
"device": {data.AnglerDevice, data.BullheadDevice, data.CrosshatchDevice},
types.PrimaryKeyField: {string(data.AlphaTest)},
types.CorpusField: {"gm"},
},
OccurrencesInTile: 6,
},
},
},
}, details)
}
func TestDigestDetailsThreeDevicesChangeList(t *testing.T) {
unittest.SmallTest(t)
const digestWeWantDetailsAbout = data.AlphaPositiveDigest
const testWeWantDetailsAbout = data.AlphaTest
const testCLID = "abc12345"
const testCRS = "gerritHub"
mes := makeThreeDevicesExpectationStore()
// Mock out some ChangeList expectations in which the digest we care about is negative
var ie expectations.Expectations
ie.Set(testWeWantDetailsAbout, digestWeWantDetailsAbout, expectations.Negative)
addChangeListExpectations(mes, testCRS, testCLID, &ie)
mds := makeDiffStoreWithNoFailures()
// There are no positive digests with which to compare
// Negative match. Note If a digest is compared to itself, it is removed from the return value.
mds.On("Get", testutils.AnyContext, digestWeWantDetailsAbout, types.DigestSlice{digestWeWantDetailsAbout, data.AlphaNegativeDigest}).
Return(map[types.Digest]*diff.DiffMetrics{
data.AlphaNegativeDigest: makeBigDiffMetric(),
}, nil)
s := New(mds, mes, nil, makeThreeDevicesIndexer(), nil, nil, emptyCommentStore(), everythingPublic)
details, err := s.GetDigestDetails(context.Background(), testWeWantDetailsAbout, digestWeWantDetailsAbout, testCLID, testCRS)
require.NoError(t, err)
assert.Equal(t, details.Digest.Status, expectations.Negative.String())
}
// TestDigestDetailsThreeDevicesOldDigest represents the scenario in which a user is requesting
// data about a digest that just went off the tile.
func TestDigestDetailsThreeDevicesOldDigest(t *testing.T) {
unittest.SmallTest(t)
const digestWeWantDetailsAbout = types.Digest("digest-too-old")
const testWeWantDetailsAbout = data.BetaTest
mds := makeDiffStoreWithNoFailures()
addDiffData(mds, digestWeWantDetailsAbout, data.BetaPositiveDigest, makeSmallDiffMetric())
s := New(mds, makeThreeDevicesExpectationStore(), nil, makeThreeDevicesIndexer(), nil, nil, nil, everythingPublic)
d, err := s.GetDigestDetails(context.Background(), testWeWantDetailsAbout, digestWeWantDetailsAbout, "", "")
require.NoError(t, err)
// spot check is fine for this test because other tests do a more thorough check of the
// whole struct.
assert.Equal(t, digestWeWantDetailsAbout, d.Digest.Digest)
assert.Equal(t, testWeWantDetailsAbout, d.Digest.Test)
assert.Equal(t, map[common.RefClosest]*frontend.SRDiffDigest{
common.PositiveRef: {
DiffMetrics: makeSmallDiffMetric(),
Digest: data.BetaPositiveDigest,
Status: "positive",
ParamSet: paramtools.ParamSet{
"device": []string{data.AnglerDevice, data.BullheadDevice},
types.PrimaryKeyField: []string{string(data.BetaTest)},
types.CorpusField: []string{"gm"},
},
OccurrencesInTile: 6,
},
common.NegativeRef: nil,
}, d.Digest.RefDiffs)
}
// TestDigestDetailsThreeDevicesOldDigest represents the scenario in which a user is requesting
// data about a digest that never existed. In the past, when this has happened, it has broken
// Gold until that digest went away (e.g. because a bot only uploaded a subset of images).
// Therefore, we shouldn't error the search request, because it could break all searches for
// untriaged digests.
func TestDigestDetailsThreeDevicesBadDigest(t *testing.T) {
unittest.SmallTest(t)
const digestWeWantDetailsAbout = types.Digest("unknown-digest")
const testWeWantDetailsAbout = data.BetaTest
mds := makeDiffStoreWithNoFailures()
mds.On("Get", testutils.AnyContext, digestWeWantDetailsAbout, types.DigestSlice{data.BetaPositiveDigest}).Return(nil, errors.New("invalid digest"))
s := New(mds, makeThreeDevicesExpectationStore(), nil, makeThreeDevicesIndexer(), nil, nil, nil, everythingPublic)
r, err := s.GetDigestDetails(context.Background(), testWeWantDetailsAbout, digestWeWantDetailsAbout, "", "")
require.NoError(t, err)
// Since we couldn't find the digest, we have nothing to compare against.
assert.Equal(t, r.Digest.Digest, digestWeWantDetailsAbout)
assert.Equal(t, r.Digest.ClosestRef, common.NoRef)
}
func TestDigestDetailsThreeDevicesBadTest(t *testing.T) {
unittest.SmallTest(t)
const digestWeWantDetailsAbout = data.AlphaPositiveDigest
const testWeWantDetailsAbout = types.TestName("invalid test")
s := New(nil, nil, nil, makeThreeDevicesIndexer(), nil, nil, nil, everythingPublic)
_, err := s.GetDigestDetails(context.Background(), testWeWantDetailsAbout, digestWeWantDetailsAbout, "", "")
require.Error(t, err)
assert.Contains(t, err.Error(), "unknown")
}
func TestDigestDetailsThreeDevicesBadTestAndDigest(t *testing.T) {
unittest.SmallTest(t)
const digestWeWantDetailsAbout = types.Digest("invalid digest")
const testWeWantDetailsAbout = types.TestName("invalid test")
s := New(nil, nil, nil, makeThreeDevicesIndexer(), nil, nil, nil, everythingPublic)
_, err := s.GetDigestDetails(context.Background(), testWeWantDetailsAbout, digestWeWantDetailsAbout, "", "")
require.Error(t, err)
assert.Contains(t, err.Error(), "unknown")
}
func TestDiffDigestsSunnyDay(t *testing.T) {
unittest.SmallTest(t)
const testWeWantDetailsAbout = data.AlphaTest
const leftDigest = data.AlphaUntriagedDigest
const rightDigest = data.AlphaPositiveDigest
mds := makeDiffStoreWithNoFailures()
addDiffData(mds, leftDigest, rightDigest, makeSmallDiffMetric())
s := New(mds, makeThreeDevicesExpectationStore(), nil, makeThreeDevicesIndexer(), nil, nil, nil, everythingPublic)
cd, err := s.DiffDigests(context.Background(), testWeWantDetailsAbout, leftDigest, rightDigest, "", "")
require.NoError(t, err)
assert.Equal(t, &frontend.DigestComparison{
Left: &frontend.SRDigest{
Test: testWeWantDetailsAbout,
Digest: leftDigest,
Status: expectations.Untriaged.String(),
ParamSet: paramtools.ParamSet{
"device": []string{data.BullheadDevice},
types.PrimaryKeyField: []string{string(data.AlphaTest)},
types.CorpusField: []string{"gm"},
},
},
Right: &frontend.SRDiffDigest{
Digest: rightDigest,
Status: expectations.Positive.String(),
DiffMetrics: makeSmallDiffMetric(),
ParamSet: paramtools.ParamSet{
"device": []string{data.AnglerDevice, data.CrosshatchDevice},
types.PrimaryKeyField: []string{string(data.AlphaTest)},
types.CorpusField: []string{"gm"},
},
},
}, cd)
}
func TestDiffDigestsChangeList(t *testing.T) {
unittest.SmallTest(t)
const testWeWantDetailsAbout = data.AlphaTest
const leftDigest = data.AlphaUntriagedDigest
const rightDigest = data.AlphaPositiveDigest
const clID = "abc12354"
const crs = "gerritHub"
mes := makeThreeDevicesExpectationStore()
var ie expectations.Expectations
ie.Set(data.AlphaTest, leftDigest, expectations.Negative)
addChangeListExpectations(mes, crs, clID, &ie)
mds := makeDiffStoreWithNoFailures()
addDiffData(mds, leftDigest, rightDigest, makeSmallDiffMetric())
s := New(mds, mes, nil, makeThreeDevicesIndexer(), nil, nil, nil, everythingPublic)
cd, err := s.DiffDigests(context.Background(), testWeWantDetailsAbout, leftDigest, rightDigest, clID, crs)
require.NoError(t, err)
assert.Equal(t, cd.Left.Status, expectations.Negative.String())
}
// TestUntriagedUnignoredTryJobExclusiveDigests_NoIndexBuilt_Success models the case where a set of
// TryJobs has produced five digests that were "untriaged on master" (and one good digest). We are
// testing that we can properly deduce which are untriaged, "newly seen" and unignored. One of
// these untriaged digests was already seen on master (data.AlphaUntriagedDigest), one was already
// triaged negative for this CL (gammaNegativeTryJobDigest), and one trace matched an ignore rule
// (deltaIgnoredTryJobDigest). Thus, we only expect tjUntriagedAlpha and tjUntriagedBeta to be
// reported to us.
func TestUntriagedUnignoredTryJobExclusiveDigests_NoIndexBuilt_Success(t *testing.T) {
unittest.SmallTest(t)
const clID = "44474"
const crs = "github"
expectedID := tjstore.CombinedPSID{
CL: clID,
CRS: crs,
PS: "abcdef",
}
const alphaUntriagedTryJobDigest = types.Digest("aaaa65e567de97c8a62918401731c7ec")
const betaUntriagedTryJobDigest = types.Digest("bbbb34f7c915a1ac3a5ba524c741946c")
const gammaNegativeTryJobDigest = types.Digest("cccc41bf4584e51be99e423707157277")
const deltaIgnoredTryJobDigest = types.Digest("dddd84e51be99e42370715727765e563")
mi := &mock_index.IndexSource{}
mtjs := &mock_tjstore.Store{}
// Set up the expectations such that for this CL, we have one extra expectation - marking
// gammaNegativeTryJobDigest negative (it would be untriaged on master).
mes := makeThreeDevicesExpectationStore()
var ie expectations.Expectations
ie.Set(data.AlphaTest, gammaNegativeTryJobDigest, expectations.Negative)
addChangeListExpectations(mes, crs, clID, &ie)
cpxTile := tiling.NewComplexTile(data.MakeTestTile())
reduced := data.MakeTestTile()
delete(reduced.Traces, data.BullheadBetaTraceID)
// The following rule exclusively matches BullheadBetaTraceID, for which the tryjob produced
// deltaIgnoredTryJobDigest
cpxTile.SetIgnoreRules(reduced, paramtools.ParamMatcher{
{
"device": []string{data.BullheadDevice},
types.PrimaryKeyField: []string{string(data.BetaTest)},
},
})
dc := digest_counter.New(data.MakeTestTile())
fis, err := indexer.SearchIndexForTesting(cpxTile, [2]digest_counter.DigestCounter{dc, dc}, [2]paramsets.ParamSummary{}, mes, nil)
require.NoError(t, err)
mi.On("GetIndex").Return(fis)
mi.On("GetIndexForCL", crs, clID).Return(nil)
anglerGroup := map[string]string{
"device": data.AnglerDevice,
}
bullheadGroup := map[string]string{
"device": data.BullheadDevice,
}
crosshatchGroup := map[string]string{
"device": data.CrosshatchDevice,
}
options := map[string]string{
"ext": "png",
}
mtjs.On("GetResults", testutils.AnyContext, expectedID).Return([]tjstore.TryJobResult{
{
GroupParams: anglerGroup,
Options: options,
Digest: betaUntriagedTryJobDigest, // should be reported
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.AlphaTest),
types.CorpusField: "gm",
},
},
{
GroupParams: bullheadGroup,
Options: options,
Digest: data.AlphaUntriagedDigest, // already seen on master as untriaged.
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.AlphaTest),
types.CorpusField: "gm",
},
},
{
GroupParams: anglerGroup,
Options: options,
Digest: alphaUntriagedTryJobDigest, // should be reported.
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.BetaTest),
types.CorpusField: "gm",
},
},
{
GroupParams: bullheadGroup,
Options: options,
Digest: deltaIgnoredTryJobDigest, // matches an ignore rule; should be filtered.
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.BetaTest),
types.CorpusField: "gm",
},
},
{
GroupParams: crosshatchGroup,
Options: options,
Digest: gammaNegativeTryJobDigest, // already triaged as negative; should be filtered.
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.AlphaTest),
types.CorpusField: "gm",
},
},
{
GroupParams: crosshatchGroup,
Options: options,
Digest: data.BetaPositiveDigest, // seen on master as positive; should be filtered.
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.BetaTest),
types.CorpusField: "gm",
},
},
}, nil).Once()
s := New(nil, mes, nil, mi, nil, mtjs, nil, everythingPublic)
dl, err := s.UntriagedUnignoredTryJobExclusiveDigests(context.Background(), expectedID)
require.NoError(t, err)
assert.Equal(t, []types.Digest{alphaUntriagedTryJobDigest, betaUntriagedTryJobDigest}, dl.Digests)
// TS should be very recent, since the results were freshly computed.
assert.True(t, dl.TS.After(time.Now().Add(-time.Minute)))
}
// TestUntriagedUnignoredTryJobExclusiveDigests_UsesIndex_Success is the same as the previous test,
// except it uses the pre-built index, which only returns the untriaged digests (that then need
// to be filtered to check for ignore rules and those seen on master).
func TestUntriagedUnignoredTryJobExclusiveDigests_UsesIndex_Success(t *testing.T) {
unittest.SmallTest(t)
const clID = "44474"
const crs = "github"
expectedID := tjstore.CombinedPSID{
CL: clID,
CRS: crs,
PS: "abcdef",
}
someOtherID := tjstore.CombinedPSID{
CL: "made up id",
CRS: crs,
PS: "gasdf",
}
const alphaUntriagedTryJobDigest = types.Digest("aaaa65e567de97c8a62918401731c7ec")
const betaUntriagedTryJobDigest = types.Digest("bbbb34f7c915a1ac3a5ba524c741946c")
const gammaNegativeTryJobDigest = types.Digest("cccc41bf4584e51be99e423707157277")
const deltaIgnoredTryJobDigest = types.Digest("dddd84e51be99e42370715727765e563")
mi := &mock_index.IndexSource{}
// Set up the expectations such that for this CL, we have one extra expectation - marking
// gammaNegativeTryJobDigest negative (it would be untriaged on master).
mes := makeThreeDevicesExpectationStore()
var ie expectations.Expectations
ie.Set(data.AlphaTest, gammaNegativeTryJobDigest, expectations.Negative)
addChangeListExpectations(mes, crs, clID, &ie)
cpxTile := tiling.NewComplexTile(data.MakeTestTile())
reduced := data.MakeTestTile()
delete(reduced.Traces, data.BullheadBetaTraceID)
// The following rule exclusively matches BullheadBetaTraceID, for which the tryjob produced
// deltaIgnoredTryJobDigest
cpxTile.SetIgnoreRules(reduced, paramtools.ParamMatcher{
{
"device": []string{data.BullheadDevice},
types.PrimaryKeyField: []string{string(data.BetaTest)},
},
})
dc := digest_counter.New(data.MakeTestTile())
fis, err := indexer.SearchIndexForTesting(cpxTile, [2]digest_counter.DigestCounter{dc, dc}, [2]paramsets.ParamSummary{}, mes, nil)
require.NoError(t, err)
mi.On("GetIndex").Return(fis)
anglerGroup := map[string]string{
"device": data.AnglerDevice,
}
bullheadGroup := map[string]string{
"device": data.BullheadDevice,
}
crosshatchGroup := map[string]string{
"device": data.CrosshatchDevice,
}
options := map[string]string{
"ext": "png",
}
indexTS := time.Date(2020, time.May, 1, 2, 3, 4, 0, time.UTC)
mi.On("GetIndexForCL", crs, clID).Return(&indexer.ChangeListIndex{
ComputedTS: indexTS,
UntriagedResultsProduced: map[tjstore.CombinedPSID][]tjstore.TryJobResult{
expectedID: {
{
GroupParams: anglerGroup,
Options: options,
Digest: betaUntriagedTryJobDigest, // should be reported.
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.AlphaTest),
types.CorpusField: "gm",
},
},
{
GroupParams: bullheadGroup,
Options: options,
Digest: data.AlphaUntriagedDigest, // already seen on master as untriaged.
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.AlphaTest),
types.CorpusField: "gm",
},
},
{
GroupParams: anglerGroup,
Options: options,
Digest: alphaUntriagedTryJobDigest, // should be reported.
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.BetaTest),
types.CorpusField: "gm",
},
},
{
GroupParams: bullheadGroup,
Options: options,
Digest: deltaIgnoredTryJobDigest, // matches an ignore rule; should be filtered.
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.BetaTest),
types.CorpusField: "gm",
},
},
{
GroupParams: crosshatchGroup,
Options: options,
Digest: gammaNegativeTryJobDigest, // already triaged as negative; should be filtered.
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.AlphaTest),
types.CorpusField: "gm",
},
},
},
someOtherID: { // this map entry should be ignored
{
GroupParams: anglerGroup,
Options: options,
Digest: "should be ignored",
ResultParams: map[string]string{
types.PrimaryKeyField: string(data.BetaTest),
types.CorpusField: "gm",
},
},
},
},
})
s := New(nil, mes, nil, mi, nil, nil, nil, everythingPublic)
dl, err := s.UntriagedUnignoredTryJobExclusiveDigests(context.Background(), expectedID)
require.NoError(t, err)
assert.Equal(t, []types.Digest{alphaUntriagedTryJobDigest, betaUntriagedTryJobDigest}, dl.Digests)
assert.Equal(t, indexTS, dl.TS)
}
// TestGetDrawableTraces_DigestIndicesAreCorrect tests that we generate the output required to draw
// the trace graphs correctly, especially when dealing with many digests or missing digests.
func TestGetDrawableTraces_DigestIndicesAreCorrect(t *testing.T) {
unittest.SmallTest(t)
// Add some shorthand aliases for easier-to-read test inputs.
const mm = tiling.MissingDigest
const mdi = missingDigestIndex
// These constants are not actual md5 digests, but that's ok for the purposes of this test -
// any string constants will do.
const d0, d1, d2, d3, d4 = types.Digest("d0"), types.Digest("d1"), types.Digest("d2"), types.Digest("d3"), types.Digest("d4")
test := func(desc string, inputDigests []types.Digest, expectedData []int) {
// stubClassifier returns Positive for everything. For the purposes of drawing traces,
// don't actually care about the expectations.
stubClassifier := &mock_expectations.Classifier{}
stubClassifier.On("Classification", mock.Anything, mock.Anything).Return(expectations.Positive)
t.Run(desc, func(t *testing.T) {
s := SearchImpl{}
traces := map[tiling.TraceID]*tiling.Trace{
"not-a-real-trace-id-and-that's-ok": {
Digests: inputDigests,
// Keys can be omitted because they are not read here,
},
}
rv := s.getDrawableTraces("whatever", d0, len(inputDigests)-1, stubClassifier, traces, nil)
require.Len(t, rv.Traces, 1)
assert.Equal(t, expectedData, rv.Traces[0].Data)
})
}
test("several distinct digests",
[]types.Digest{d4, d3, d2, d1, d0},
[]int{4, 3, 2, 1, 0})
// index 1 represents the first digest, starting at head, that doesn't match the "digest of
// focus", which for these tests is d0. For convenience, in all the other sub-tests, the index
// on the constants matches the expected index.
test("several distinct digests, ordered by proximity to head",
[]types.Digest{d1, d2, d3, d4, d0},
[]int{4, 3, 2, 1, 0})
test("missing digests",
[]types.Digest{mm, d1, mm, d0, mm},
[]int{mdi, 1, mdi, 0, mdi})
test("multiple missing digest in a row",
[]types.Digest{mm, mm, mm, d1, d1, mm, mm, mm, d0, mm, mm},
[]int{mdi, mdi, mdi, 1, 1, mdi, mdi, mdi, 0, mdi, mdi})
test("all the same",
[]types.Digest{d0, d0, d0, d0, d0, d0, d0},
[]int{0, 0, 0, 0, 0, 0, 0})
test("d0 not at head",
[]types.Digest{d0, d0, d0, d1, d2, d1},
[]int{0, 0, 0, 1, 2, 1})
// At a certain point, we lump distinct digests together. Currently this is after we have seen
// 8 distinct digests (starting at head).
test("too many distinct digests",
[]types.Digest{"dA", "d9", "d8", "d7", "d6", "d5", d4, d3, d2, d1, d0},
[]int{8, 8, 8, 7, 6, 5, 4, 3, 2, 1, 0})
}
// TestGetDrawableTraces_TotalDigestsCorrect tests that we count unique digests for a TraceGroup
// correctly, even when there are multiple traces or the number of digests is bigger than
// maxDistinctDigestsToPresent.
func TestGetDrawableTraces_TotalDigestsCorrect(t *testing.T) {
unittest.SmallTest(t)
// Add some shorthand aliases for easier-to-read test inputs.
const md = tiling.MissingDigest
// This constant is not an actual md5 digest, but that's ok for the purposes of this test -
// any string constants will do.
const d0 = types.Digest("d0")
test := func(desc string, totalUniqueDigests int, inputTraceDigests ...[]types.Digest) {
// stubClassifier returns Positive for everything. For the purposes of counting digests,
// don't actually care about the expectations.
stubClassifier := &mock_expectations.Classifier{}
stubClassifier.On("Classification", mock.Anything, mock.Anything).Return(expectations.Positive)
t.Run(desc, func(t *testing.T) {
s := SearchImpl{}
traces := map[tiling.TraceID]*tiling.Trace{}
for i, digests := range inputTraceDigests {
id := tiling.TraceID(fmt.Sprintf("trace-%d", i))
traces[id] = &tiling.Trace{
Digests: digests,
// Keys can be omitted because they are not read here,
}
}
rv := s.getDrawableTraces("whatever", d0, len(inputTraceDigests[0])-1, stubClassifier, traces, nil)
require.Len(t, rv.Traces, len(inputTraceDigests))
assert.Equal(t, totalUniqueDigests, rv.TotalDigests)
})
}
test("one distinct digest, repeated multiple times",
1,
[]types.Digest{d0, d0, d0})
test("several distinct digests",
5,
[]types.Digest{"d4", "d3", "d2", "d1", d0})
test("does not count missing digests",
5,
[]types.Digest{"d4", "d3", "d2", "d1", md, d0},
[]types.Digest{md, md, md, md, d0, md})
test("accounts for distinct digests across traces",
13,
[]types.Digest{"d6", "d5", "d4", "d3", "d2", "d1", d0},
[]types.Digest{"dF", "dE", "dD", "dC", "dB", "dA", d0})
test("one long trace",
11,
[]types.Digest{"dA", "d9", "d8", "d7", "d6", "d5", "d4", "d3", "d2", "d1", d0})
}
func TestGetDigestRecs_Success(t *testing.T) {
unittest.SmallTest(t)
sr := getDigestRecs(srInterMap{
data.AlphaTest: {
data.AlphaPositiveDigest: {
test: data.AlphaTest,
digest: data.AlphaPositiveDigest,
},
data.AlphaNegativeDigest: {
test: data.AlphaTest,
digest: data.AlphaNegativeDigest,
},
},
data.BetaTest: {
data.BetaPositiveDigest: {
test: data.BetaTest,
digest: data.BetaPositiveDigest,
},
data.BetaUntriagedDigest: {
test: data.BetaTest,
digest: data.BetaUntriagedDigest,
},
},
}, data.MakeTestExpectations())
assert.ElementsMatch(t, []*frontend.SRDigest{
{
Test: data.AlphaTest,
Digest: data.AlphaPositiveDigest,
Status: expectations.Positive.String(),
},
{
Test: data.AlphaTest,
Digest: data.AlphaNegativeDigest,
Status: expectations.Negative.String(),
},
{
Test: data.BetaTest,
Digest: data.BetaPositiveDigest,
Status: expectations.Positive.String(),
},
{
Test: data.BetaTest,
Digest: data.BetaUntriagedDigest,
Status: expectations.Untriaged.String(),
},
}, sr)
}
func TestAddTriageHistory_HistoryExistsForAllEntries_Success(t *testing.T) {
unittest.SmallTest(t)
mes := makeThreeDevicesExpectationStore()
s := New(nil, mes, nil, nil, nil, nil, nil, nil)
input := []*frontend.SRDigest{
{
Test: data.AlphaTest,
Digest: data.AlphaPositiveDigest,
// The rest of the fields don't matter for this test.
},
{
Test: data.AlphaTest,
Digest: data.AlphaNegativeDigest,
},
{
Test: data.BetaTest,
Digest: data.BetaPositiveDigest,
},
}
s.addTriageHistory(context.Background(), input)
assert.Equal(t, []frontend.TriageHistory{
{
User: userWhoTriaged,
TS: alphaPositiveTriageTS,
},
}, input[0].TriageHistory)
assert.Equal(t, []frontend.TriageHistory{
{
User: userWhoTriaged,
TS: alphaNegativeTriageTS,
},
}, input[1].TriageHistory)
assert.Equal(t, []frontend.TriageHistory{
{
User: userWhoTriaged,
TS: betaPositiveTriageTS,
},
}, input[2].TriageHistory)
}
func TestAddTriageHistory_EmptyTriageHistory_Success(t *testing.T) {
unittest.SmallTest(t)
mes := &mock_expectations.Store{}
mes.On("GetTriageHistory", testutils.AnyContext, mock.Anything, mock.Anything).Return(nil, nil)
s := New(nil, mes, nil, nil, nil, nil, nil, nil)
input := []*frontend.SRDigest{
{
Test: data.AlphaTest,
Digest: data.AlphaPositiveDigest,
// The rest of the fields don't matter for this test.
},
{
Test: data.AlphaTest,
Digest: data.AlphaNegativeDigest,
},
{
Test: data.BetaTest,
Digest: data.BetaPositiveDigest,
},
}
s.addTriageHistory(context.Background(), input)
assert.Nil(t, input[0].TriageHistory)
assert.Nil(t, input[1].TriageHistory)
assert.Nil(t, input[2].TriageHistory)
}
func TestAddTriageHistory_ExpectationStoreError_ReturnedTriageHistoryIsEmpty(t *testing.T) {
unittest.SmallTest(t)
mes := &mock_expectations.Store{}
mes.On("GetTriageHistory", testutils.AnyContext, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("kaboom"))
s := New(nil, mes, nil, nil, nil, nil, nil, nil)
input := []*frontend.SRDigest{
{
Test: data.AlphaTest,
Digest: data.AlphaPositiveDigest,
// The rest of the fields don't matter for this test.
},
}
s.addTriageHistory(context.Background(), input)
assert.Nil(t, input[0].TriageHistory)
}
func TestGetTriageHistory_CachesResults_CallsGetTriageHistoryOncePerEntry(t *testing.T) {
unittest.SmallTest(t)
mes := &mock_expectations.Store{}
mes.On("GetTriageHistory", testutils.AnyContext, data.AlphaTest, data.AlphaPositiveDigest).Return([]expectations.TriageHistory{
{
User: userWhoTriaged,
TS: alphaPositiveTriageTS,
},
}, nil).Once()
mes.On("GetTriageHistory", testutils.AnyContext, data.BetaTest, data.BetaPositiveDigest).Return([]expectations.TriageHistory{
{
User: userWhoTriaged,
TS: betaPositiveTriageTS,
},
}, nil).Once()
s := New(nil, mes, nil, nil, nil, nil, nil, nil)
trBeta := s.getTriageHistory(context.Background(), data.BetaTest, data.BetaPositiveDigest)
assert.Equal(t, []frontend.TriageHistory{
{
User: userWhoTriaged,
TS: betaPositiveTriageTS,
},
}, trBeta)
trAlpha := s.getTriageHistory(context.Background(), data.AlphaTest, data.AlphaPositiveDigest)
assert.Equal(t, []frontend.TriageHistory{
{
User: userWhoTriaged,
TS: alphaPositiveTriageTS,
},
}, trAlpha)
// This should be from the cache - the .Once() on the mocks ensures that.
tr := s.getTriageHistory(context.Background(), data.BetaTest, data.BetaPositiveDigest)
assert.Equal(t, trBeta, tr)
tr = s.getTriageHistory(context.Background(), data.AlphaTest, data.AlphaPositiveDigest)
assert.Equal(t, trAlpha, tr)
}
func TestGetTriageHistory_CacheClearedWhenNotified(t *testing.T) {
unittest.SmallTest(t)
notifier := expectations.NewEventDispatcherForTesting()
mes := &mock_expectations.Store{}
mes.On("GetTriageHistory", testutils.AnyContext, data.AlphaTest, data.AlphaPositiveDigest).Return(nil, nil).Once()
s := New(nil, mes, notifier, nil, nil, nil, nil, nil)
// The first call to history is empty.
tr := s.getTriageHistory(context.Background(), data.AlphaTest, data.AlphaPositiveDigest)
assert.Empty(t, tr)
// Notify something that does not match our entry.
notifier.NotifyChange(expectations.ID{
Grouping: data.AlphaTest,
Digest: "for a completely different digest",
})
// The empty value should still be cached.
tr = s.getTriageHistory(context.Background(), data.AlphaTest, data.AlphaPositiveDigest)
assert.Empty(t, tr)
mes.On("GetTriageHistory", testutils.AnyContext, data.AlphaTest, data.AlphaPositiveDigest).Return([]expectations.TriageHistory{
{
User: userWhoTriaged,
TS: alphaPositiveTriageTS,
},
}, nil).Once()
notifier.NotifyChange(expectations.ID{
Grouping: data.AlphaTest,
Digest: data.AlphaPositiveDigest,
})
// Cache should be cleared for our entry, so we refetch and get the new results.
trAlpha := s.getTriageHistory(context.Background(), data.AlphaTest, data.AlphaPositiveDigest)
assert.Equal(t, []frontend.TriageHistory{
{
User: userWhoTriaged,
TS: alphaPositiveTriageTS,
},
}, trAlpha)
// This should now be from the cache.
tr = s.getTriageHistory(context.Background(), data.AlphaTest, data.AlphaPositiveDigest)
assert.Equal(t, trAlpha, tr)
}
var everythingPublic = paramtools.ParamSet{}
// makeThreeDevicesIndexer returns an IndexSource that returns the result of makeThreeDevicesIndex.
func makeThreeDevicesIndexer() indexer.IndexSource {
mi := &mock_index.IndexSource{}
fis := makeThreeDevicesIndex()
mi.On("GetIndex").Return(fis)
mi.On("GetIndexForCL", mock.Anything, mock.Anything).Return(nil)
return mi
}
// makeThreeDevicesIndex returns a search index corresponding to the three_devices_data
// (which currently has nothing ignored).
func makeThreeDevicesIndex() *indexer.SearchIndex {
cpxTile := tiling.NewComplexTile(data.MakeTestTile())
dc := digest_counter.New(data.MakeTestTile())
ps := paramsets.NewParamSummary(data.MakeTestTile(), dc)
si, err := indexer.SearchIndexForTesting(cpxTile, [2]digest_counter.DigestCounter{dc, dc}, [2]paramsets.ParamSummary{ps, ps}, nil, nil)
if err != nil {
// Something is horribly broken with our test data/setup
panic(err.Error())
}
return si
}
const userWhoTriaged = "test@example.com"
var (
alphaPositiveTriageTS = time.Date(2020, time.March, 1, 2, 3, 4, 0, time.UTC)
alphaNegativeTriageTS = time.Date(2020, time.March, 4, 2, 3, 4, 0, time.UTC)
betaPositiveTriageTS = time.Date(2020, time.March, 7, 2, 3, 4, 0, time.UTC)
)
func makeThreeDevicesExpectationStore() *mock_expectations.Store {
mes := &mock_expectations.Store{}
mes.On("Get", testutils.AnyContext).Return(data.MakeTestExpectations(), nil)
mes.On("GetTriageHistory", testutils.AnyContext, data.AlphaTest, data.AlphaPositiveDigest).Return([]expectations.TriageHistory{
{
User: userWhoTriaged,
TS: alphaPositiveTriageTS,
},
}, nil)
mes.On("GetTriageHistory", testutils.AnyContext, data.AlphaTest, data.AlphaNegativeDigest).Return([]expectations.TriageHistory{
{
User: userWhoTriaged,
TS: alphaNegativeTriageTS,
},
}, nil)
mes.On("GetTriageHistory", testutils.AnyContext, data.BetaTest, data.BetaPositiveDigest).Return([]expectations.TriageHistory{
{
User: userWhoTriaged,
TS: betaPositiveTriageTS,
},
}, nil)
// Catch-all for the untriaged entries
mes.On("GetTriageHistory", testutils.AnyContext, mock.Anything, mock.Anything).Return(nil, nil)
return mes
}
func addChangeListExpectations(mes *mock_expectations.Store, crs string, clID string, issueExp *expectations.Expectations) {
issueStore := &mock_expectations.Store{}
mes.On("ForChangeList", clID, crs).Return(issueStore, nil)
issueStore.On("Get", testutils.AnyContext).Return(issueExp, nil)
}
func makeDiffStoreWithNoFailures() *mock_diffstore.DiffStore {
mds := &mock_diffstore.DiffStore{}
mds.On("UnavailableDigests", testutils.AnyContext).Return(map[types.Digest]*diff.DigestFailure{}, nil)
return mds
}
func addDiffData(mds *mock_diffstore.DiffStore, left types.Digest, right types.Digest, metric *diff.DiffMetrics) {
if metric == nil {
// empty map is expected instead of a nil entry
mds.On("Get", testutils.AnyContext, left, types.DigestSlice{right}).
Return(map[types.Digest]*diff.DiffMetrics{}, nil)
} else {
mds.On("Get", testutils.AnyContext, left, types.DigestSlice{right}).
Return(map[types.Digest]*diff.DiffMetrics{
right: metric,
}, nil)
}
}
// This is arbitrary data.
func makeSmallDiffMetric() *diff.DiffMetrics {
return &diff.DiffMetrics{
NumDiffPixels: 8,
PixelDiffPercent: 0.02,
MaxRGBADiffs: [4]int{0, 48, 12, 0},
DimDiffer: false,
Diffs: map[string]float32{
diff.CombinedMetric: 0.0005,
"percent": 0.02,
"pixel": 8,
},
}
}
func makeBigDiffMetric() *diff.DiffMetrics {
return &diff.DiffMetrics{
NumDiffPixels: 88812,
PixelDiffPercent: 98.68,
MaxRGBADiffs: [4]int{102, 51, 13, 0},
DimDiffer: true,
Diffs: map[string]float32{
diff.CombinedMetric: 4.7,
"percent": 98.68,
"pixel": 88812,
},
}
}
func emptyCommentStore() comment.Store {
mcs := &mock_comment.Store{}
mcs.On("ListComments", testutils.AnyContext).Return(nil, nil)
return mcs
}
// makeStubDiffStore returns a diffstore that returns the small diff metric for every call to Get.
func makeStubDiffStore() *mock_diffstore.DiffStore {
mds := &mock_diffstore.DiffStore{}
mds.On("UnavailableDigests", testutils.AnyContext).Return(map[types.Digest]*diff.DigestFailure{}, nil)
mds.On("Get", testutils.AnyContext, mock.Anything, mock.Anything).Return(func(_ context.Context, _ types.Digest, rights types.DigestSlice) map[types.Digest]*diff.DiffMetrics {
rv := make(map[types.Digest]*diff.DiffMetrics, len(rights))
for _, right := range rights {
rv[right] = makeSmallDiffMetric()
}
return rv
}, nil)
return mds
}