[bugs-central] IssueTracker implementation of the BugFramework interface
Bug: skia:10783
Change-Id: Ib65695433cd515538a03fc281cf9e178b667926d
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/328990
Reviewed-by: Joe Gregorio <jcgregorio@google.com>
diff --git a/bugs-central/go/bugs/issuetracker/issuetracker.go b/bugs-central/go/bugs/issuetracker/issuetracker.go
new file mode 100644
index 0000000..5dae1e5
--- /dev/null
+++ b/bugs-central/go/bugs/issuetracker/issuetracker.go
@@ -0,0 +1,160 @@
+package issuetracker
+
+// Accesses issuetracker results from Google storage.
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ "cloud.google.com/go/storage"
+
+ "go.skia.org/infra/bugs-central/go/bugs"
+ "go.skia.org/infra/bugs-central/go/db"
+ "go.skia.org/infra/bugs-central/go/types"
+ "go.skia.org/infra/go/skerr"
+ "go.skia.org/infra/go/sklog"
+ "go.skia.org/infra/go/util"
+)
+
+var (
+ issueTrackerBucket = "skia-issuetracker-details"
+ // The file that contains issuetracker search results in the above bucket.
+ resultsFileName = "results.json"
+
+ issueTrackerSource types.IssueSource = "Buganizer"
+)
+
+type issueTrackerIssue struct {
+ Id int64 `json:"id"`
+ Status string `json:"status"`
+ Priority string `json:"priority"`
+ Assignee string `json:"assignee"`
+ CreatedTS int64 `json:"created_ts"`
+ ModifiedTS int64 `json:"modified_ts"`
+}
+
+// issueTracker implements bugs.BugsFramework for github repos.
+type issueTracker struct {
+ storageClient *storage.Client
+ openIssues *bugs.OpenIssues
+ queryConfig *IssueTrackerQueryConfig
+}
+
+// New returns an instance of the issuetracker implementation of bugs.BugFramework.
+func New(storageClient *storage.Client, openIssues *bugs.OpenIssues, queryConfig *IssueTrackerQueryConfig) (bugs.BugFramework, error) {
+ return &issueTracker{
+ storageClient: storageClient,
+ openIssues: openIssues,
+ queryConfig: queryConfig,
+ }, nil
+}
+
+// IssueTrackerQueryConfig is the config that will be used when querying issuetracker.
+type IssueTrackerQueryConfig struct {
+ // Key to find the open bugs from the storage results file.
+ Query string
+ // Which client's issues we are looking for.
+ Client types.RecognizedClient
+ // Issues are considered untriaged if they have any of these priorities.
+ UntriagedPriorities []string
+ // Issues are also considered untriaged if they are assigned to any of these emails.
+ UntriagedAliases []string
+}
+
+// See documentation for bugs.Search interface.
+func (it *issueTracker) Search(ctx context.Context) ([]*types.Issue, *types.IssueCountsData, error) {
+ obj := it.storageClient.Bucket(issueTrackerBucket).Object(resultsFileName)
+ reader, err := obj.NewReader(ctx)
+ if err != nil {
+ return nil, nil, skerr.Wrapf(err, "accessing gs://%s/%s failed", issueTrackerBucket, resultsFileName)
+ }
+ defer util.Close(reader)
+
+ var results map[string][]issueTrackerIssue
+ if err := json.NewDecoder(reader).Decode(&results); err != nil {
+ return nil, nil, skerr.Wrapf(err, "invalid JSON from %s", resultsFileName)
+ }
+
+ if _, ok := results[it.queryConfig.Query]; !ok {
+ return nil, nil, skerr.Fmt("could not find %s in %s", it.queryConfig.Query, resultsFileName)
+ }
+ // These are all the issues returned by the issuetracker query.
+ trackerIssues := results[it.queryConfig.Query]
+
+ // Convert issuetracker issues into bug_framework's generic issues
+ issues := []*types.Issue{}
+ countsData := &types.IssueCountsData{}
+ for _, i := range trackerIssues {
+ // Populate counts data.
+ countsData.OpenCount++
+ if i.Assignee == "" {
+ countsData.UnassignedCount++
+ }
+ created := time.Unix(i.CreatedTS, 0)
+ modified := time.Unix(i.ModifiedTS, 0)
+ priority := types.StandardizedPriority(i.Priority)
+ countsData.IncPriority(priority)
+ countsData.CalculateSLOViolations(time.Now(), created, modified, priority)
+ if util.In(i.Priority, it.queryConfig.UntriagedPriorities) {
+ countsData.UntriagedCount++
+ } else if util.In(i.Assignee, it.queryConfig.UntriagedAliases) {
+ countsData.UntriagedCount++
+ }
+
+ id := strconv.FormatInt(i.Id, 10)
+ issues = append(issues, &types.Issue{
+ Id: id,
+ State: i.Status,
+ Priority: priority,
+ Owner: i.Assignee,
+ CreatedTime: created,
+ ModifiedTime: modified,
+
+ Link: it.GetIssueLink("", id),
+ })
+ }
+ return issues, countsData, nil
+}
+
+// See documentation for bugs.SearchClientAndPersist interface.
+func (it *issueTracker) SearchClientAndPersist(ctx context.Context, dbClient *db.FirestoreDB, runId string) error {
+ qc := it.queryConfig
+ issues, countsData, err := it.Search(ctx)
+ if err != nil {
+ return skerr.Wrapf(err, "error when searching issuetracker")
+ }
+ sklog.Infof("%s issuetracker issues %+v", qc.Client, countsData)
+
+ queryDesc := qc.Query
+ countsData.QueryLink = fmt.Sprintf("http://b/issues?q=%s", qc.Query)
+ // Construct query for untriaged issues.
+ if len(qc.UntriagedPriorities) > 0 || len(qc.UntriagedAliases) > 0 {
+ untriagedPrioritiesTokens := []string{}
+ for _, p := range qc.UntriagedPriorities {
+ untriagedPrioritiesTokens = append(untriagedPrioritiesTokens, fmt.Sprintf("p:%s", p))
+ }
+ untriagedAliasesTokens := []string{}
+ for _, a := range qc.UntriagedAliases {
+ untriagedAliasesTokens = append(untriagedAliasesTokens, fmt.Sprintf("assignee:%s", a))
+ }
+ countsData.UntriagedQueryLink = fmt.Sprintf("%s (%s|%s)", countsData.QueryLink, strings.Join(untriagedPrioritiesTokens, "|"), strings.Join(untriagedAliasesTokens, "|"))
+ }
+ client := qc.Client
+
+ // Put in DB.
+ if err := dbClient.PutInDB(ctx, client, issueTrackerSource, queryDesc, runId, countsData); err != nil {
+ return skerr.Wrapf(err, "error putting issuetracker results in DB")
+ }
+ // Put in memory.
+ it.openIssues.PutOpenIssues(client, issueTrackerSource, queryDesc, issues)
+ return nil
+}
+
+// See documentation for bugs.GetIssueLink interface.
+func (it *issueTracker) GetIssueLink(_, id string) string {
+ return fmt.Sprintf("http://b/%s", id)
+}
diff --git a/bugs-central/go/bugs/issuetracker/issuetracker_test.go b/bugs-central/go/bugs/issuetracker/issuetracker_test.go
new file mode 100644
index 0000000..39759ab
--- /dev/null
+++ b/bugs-central/go/bugs/issuetracker/issuetracker_test.go
@@ -0,0 +1,55 @@
+package issuetracker
+
+import (
+ "context"
+ "testing"
+
+ "cloud.google.com/go/storage"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/api/option"
+
+ "go.skia.org/infra/bugs-central/go/bugs"
+ "go.skia.org/infra/go/httputils"
+ "go.skia.org/infra/go/testutils/unittest"
+)
+
+const (
+ gcsTestBucket = "skia-infra-testdata"
+ testResultsFileName = "issuetracker-results.json"
+)
+
+func TestIssueTrackerSearch(t *testing.T) {
+ unittest.LargeTest(t)
+ ctx := context.Background()
+
+ // The test bucket is a public bucket, so we don't need to worry about authentication.
+ unauthedClient := httputils.DefaultClientConfig().Client()
+
+ storageClient, err := storage.NewClient(ctx, option.WithHTTPClient(unauthedClient))
+ require.NoError(t, err)
+ issueTrackerBucket = gcsTestBucket
+ resultsFileName = testResultsFileName
+
+ it, err := New(storageClient, bugs.InitOpenIssues(), &IssueTrackerQueryConfig{
+ Query: "componentid:1346 status:open",
+ Client: "Android",
+ })
+ require.NoError(t, err)
+ issues, countsData, err := it.Search(ctx)
+ require.NoError(t, err)
+ require.Equal(t, 24, len(issues))
+ require.Equal(t, 0, countsData.P0Count)
+ require.Equal(t, 0, countsData.P1Count)
+ require.Equal(t, 11, countsData.P2Count)
+ require.Equal(t, 4, countsData.P3Count)
+ require.Equal(t, 9, countsData.P4Count)
+
+ // Use a query that does not match. Should throw an error.
+ it, err = New(storageClient, bugs.InitOpenIssues(), &IssueTrackerQueryConfig{
+ Query: "does not match",
+ Client: "Android",
+ })
+ require.NoError(t, err)
+ _, _, err = it.Search(ctx)
+ require.Error(t, err)
+}