[gold] Add JSON endpoint to query untriaged digests for a CL.
This is for https://github.com/flutter/flutter/issues/48744
Change-Id: I5bde2607deb46d0f49535a5fdc86f3ec8febfb28
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/264571
Commit-Queue: Kevin Lubick <kjlubick@google.com>
Reviewed-by: Leandro Lovisolo <lovisolo@google.com>
diff --git a/golden/cmd/skiacorrectness/main.go b/golden/cmd/skiacorrectness/main.go
index f38d4e6..42bfc38 100644
--- a/golden/cmd/skiacorrectness/main.go
+++ b/golden/cmd/skiacorrectness/main.go
@@ -521,6 +521,7 @@
jsonRouter.HandleFunc(trim("/json/triagelog/undo"), handlers.TriageUndoHandler).Methods("POST")
jsonRouter.HandleFunc(trim("/json/changelists"), handlers.ChangeListsHandler).Methods("GET")
jsonRouter.HandleFunc(trim("/json/changelist/{system}/{id}"), handlers.ChangeListSummaryHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/changelist/{system}/{id}/{patchset}/untriaged"), handlers.ChangeListUntriagedHandler).Methods("GET")
jsonRouter.HandleFunc(trim("/json/digests"), handlers.DigestListHandler).Methods("GET")
// Retrieving that baseline for master and an Gerrit issue are handled the same way
diff --git a/golden/go/search/frontend/types.go b/golden/go/search/frontend/types.go
index 101dfc1..ce5d004 100644
--- a/golden/go/search/frontend/types.go
+++ b/golden/go/search/frontend/types.go
@@ -86,3 +86,8 @@
Left *SRDigest `json:"left"` // The left hand digest and its params.
Right *SRDiffDigest `json:"right"` // The right hand digest, its params and the diff result.
}
+
+// DigestList represents multiple returned digests.
+type DigestList struct {
+ Digests []types.Digest `json:"digests"`
+}
diff --git a/golden/go/search/search.go b/golden/go/search/search.go
index 78f015c..21724df 100644
--- a/golden/go/search/search.go
+++ b/golden/go/search/search.go
@@ -685,5 +685,54 @@
return -1
}
+// UntriagedUnignoredTryJobExclusiveDigests implements the SearchAPI interface. It uses the cached
+// TryJobResults, so as to improve performance.
+// TODO(kjlubick) when we have indexes for changelist results, use those.
+func (s *SearchImpl) UntriagedUnignoredTryJobExclusiveDigests(ctx context.Context, psID tjstore.CombinedPSID) (*frontend.DigestList, error) {
+ xtr, err := s.getTryJobResults(ctx, psID)
+ if err != nil {
+ return nil, skerr.Wrapf(err, "getting tryjob results for %v", psID)
+ }
+ exp, err := s.getExpectations(ctx, psID.CL, psID.CRS)
+ if err != nil {
+ return nil, skerr.Wrap(err)
+ }
+
+ idx := s.indexSource.GetIndex()
+ ignoreMatcher := idx.GetIgnoreMatcher()
+ knownDigestsForTest := idx.DigestCountsByTest(types.IncludeIgnoredTraces)
+
+ var returnDigests []types.Digest
+
+ for _, tr := range xtr {
+ if err := ctx.Err(); err != nil {
+ return nil, skerr.Wrap(err)
+ }
+ tn := types.TestName(tr.ResultParams[types.PRIMARY_KEY_FIELD])
+ if exp.Classification(tn, tr.Digest) != expectations.Untriaged {
+ // It's been triaged already.
+ continue
+ }
+ if _, ok := knownDigestsForTest[tn][tr.Digest]; ok {
+ // It's already been seen on master
+ continue
+ }
+ p := make(paramtools.Params, len(tr.ResultParams)+len(tr.GroupParams)+len(tr.Options))
+ p.Add(tr.GroupParams)
+ p.Add(tr.Options)
+ p.Add(tr.ResultParams)
+ if ignoreMatcher.MatchAnyParams(p) {
+ // This trace matches an ignore
+ continue
+ }
+ returnDigests = append(returnDigests, tr.Digest)
+ }
+ // Sort digests alphabetically for determinism.
+ sort.Slice(returnDigests, func(i, j int) bool {
+ return returnDigests[i] < returnDigests[j]
+ })
+ return &frontend.DigestList{Digests: returnDigests}, nil
+}
+
// Make sure SearchImpl fulfills the SearchAPI interface.
var _ SearchAPI = (*SearchImpl)(nil)
diff --git a/golden/go/search/search_test.go b/golden/go/search/search_test.go
index c0f00cd..4067baf 100644
--- a/golden/go/search/search_test.go
+++ b/golden/go/search/search_test.go
@@ -1093,6 +1093,137 @@
assert.Equal(t, cd.Left.Status, expectations.Negative.String())
}
+// TestUntriagedUnignoredTryJobExclusiveDigestsSunnyDay 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.AlphaUntriaged1Digest), 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 TestUntriagedUnignoredTryJobExclusiveDigestsSunnyDay(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")
+
+ mes := &mocks.ExpectationsStore{}
+ mi := &mock_index.IndexSource{}
+ issueStore := &mocks.ExpectationsStore{}
+ mtjs := &mock_tjstore.Store{}
+ defer mes.AssertExpectations(t)
+ defer mi.AssertExpectations(t)
+ defer issueStore.AssertExpectations(t)
+ defer mtjs.AssertExpectations(t)
+
+ cpxTile := types.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.PRIMARY_KEY_FIELD: []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)
+
+ // Set up the expectations such that for this CL, we have one extra expectation - marking
+ // gammaNegativeTryJobDigest negative (it would be untriaged on master).
+ mes.On("ForChangeList", clID, crs).Return(issueStore, nil)
+ var ie expectations.Expectations
+ ie.Set(data.AlphaTest, gammaNegativeTryJobDigest, expectations.Negative)
+ issueStore.On("Get", testutils.AnyContext).Return(&ie, nil)
+ mes.On("Get", testutils.AnyContext).Return(data.MakeTestExpectations(), 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,
+ ResultParams: map[string]string{
+ types.PRIMARY_KEY_FIELD: string(data.AlphaTest),
+ types.CORPUS_FIELD: "gm",
+ },
+ },
+ {
+ GroupParams: bullheadGroup,
+ Options: options,
+ Digest: data.AlphaUntriaged1Digest,
+ ResultParams: map[string]string{
+ types.PRIMARY_KEY_FIELD: string(data.AlphaTest),
+ types.CORPUS_FIELD: "gm",
+ },
+ },
+ {
+ GroupParams: anglerGroup,
+ Options: options,
+ Digest: alphaUntriagedTryJobDigest,
+ ResultParams: map[string]string{
+ types.PRIMARY_KEY_FIELD: string(data.BetaTest),
+ types.CORPUS_FIELD: "gm",
+ },
+ },
+ {
+ GroupParams: bullheadGroup,
+ Options: options,
+ Digest: deltaIgnoredTryJobDigest,
+ ResultParams: map[string]string{
+ types.PRIMARY_KEY_FIELD: string(data.BetaTest),
+ types.CORPUS_FIELD: "gm",
+ },
+ },
+ {
+ GroupParams: crosshatchGroup,
+ Options: options,
+ Digest: gammaNegativeTryJobDigest,
+ ResultParams: map[string]string{
+ types.PRIMARY_KEY_FIELD: string(data.AlphaTest),
+ types.CORPUS_FIELD: "gm",
+ },
+ },
+ {
+ GroupParams: crosshatchGroup,
+ Options: options,
+ Digest: data.BetaGood1Digest,
+ ResultParams: map[string]string{
+ types.PRIMARY_KEY_FIELD: string(data.BetaTest),
+ types.CORPUS_FIELD: "gm",
+ },
+ },
+ }, nil).Once()
+
+ s := New(nil, mes, mi, nil, mtjs, everythingPublic)
+
+ dl, err := s.UntriagedUnignoredTryJobExclusiveDigests(context.Background(), expectedID)
+ require.NoError(t, err)
+ assert.Equal(t, []types.Digest{alphaUntriagedTryJobDigest, betaUntriagedTryJobDigest}, dl.Digests)
+}
+
var everythingPublic = paramtools.ParamSet{}
// makeThreeDevicesIndex returns a search index corresponding to the three_devices_data
diff --git a/golden/go/search/types.go b/golden/go/search/types.go
index c2f539b..bcf5219 100644
--- a/golden/go/search/types.go
+++ b/golden/go/search/types.go
@@ -5,6 +5,7 @@
"go.skia.org/infra/golden/go/search/frontend"
"go.skia.org/infra/golden/go/search/query"
+ "go.skia.org/infra/golden/go/tjstore"
"go.skia.org/infra/golden/go/types"
)
@@ -25,4 +26,9 @@
// the expectations from the given CL and Code Review System (e.g. "gerrit") will be used
// instead of those at master.
DiffDigests(ctx context.Context, t types.TestName, left, right types.Digest, clID string, crs string) (*frontend.DigestComparison, error)
+
+ // UntriagedUnignoredTryJobExclusiveDigests returns a list of untriaged, non-ignored,
+ // digests that are exclusive to a given CL (i.e. they don't also show up on the master branch
+ // in the last N commits).
+ UntriagedUnignoredTryJobExclusiveDigests(context.Context, tjstore.CombinedPSID) (*frontend.DigestList, error)
}
diff --git a/golden/go/web/web.go b/golden/go/web/web.go
index 16deca2..89126e0 100644
--- a/golden/go/web/web.go
+++ b/golden/go/web/web.go
@@ -479,6 +479,43 @@
}, nil
}
+// ChangeListUntriagedHandler writes out a list of untriaged digests uploaded by this CL that
+// are not on master already and are not ignored.
+func (wh *Handlers) ChangeListUntriagedHandler(w http.ResponseWriter, r *http.Request) {
+ defer metrics2.FuncTimer().Stop()
+ if err := wh.cheapLimitForAnonUsers(r); err != nil {
+ httputils.ReportError(w, err, "Try again later", http.StatusInternalServerError)
+ return
+ }
+
+ clID, ok := mux.Vars(r)["id"]
+ if !ok {
+ http.Error(w, "Must specify 'id' of ChangeList.", http.StatusBadRequest)
+ return
+ }
+ psID, ok := mux.Vars(r)["patchset"]
+ if !ok {
+ http.Error(w, "Must specify 'patchset' of ChangeList.", http.StatusBadRequest)
+ return
+ }
+ crs, ok := mux.Vars(r)["system"]
+ if !ok {
+ http.Error(w, "Must specify 'system' of ChangeList.", http.StatusBadRequest)
+ return
+ }
+
+ dl, err := wh.SearchAPI.UntriagedUnignoredTryJobExclusiveDigests(r.Context(), tjstore.CombinedPSID{
+ CL: clID,
+ CRS: crs,
+ PS: psID,
+ })
+ if err != nil {
+ httputils.ReportError(w, err, "could not retrieve untriaged digests for CL.", http.StatusInternalServerError)
+ return
+ }
+ sendJSONResponse(w, dl)
+}
+
// SearchHandler is the endpoint for all searches, including accessing
// results that belong to a tryjob. It times out after 3 minutes, to prevent outstanding requests
// from growing unbounded.