blob: 5b474d7ddb019f1ed98af7792d1905ef1ad5e6b6 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"context"
"fmt"
"sort"
"sync"
"testing"
"time"
"github.com/golang/protobuf/proto"
. "github.com/smartystreets/goconvey/convey"
"go.chromium.org/luci/auth/identity"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/clock/testclock"
ds "go.chromium.org/luci/gae/service/datastore"
tq "go.chromium.org/luci/gae/service/taskqueue"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/authtest"
gr "golang.org/x/build/gerrit"
admin "infra/tricium/api/admin/v1"
tricium "infra/tricium/api/v1"
"infra/tricium/appengine/common"
gc "infra/tricium/appengine/common/gerrit"
"infra/tricium/appengine/common/track"
"infra/tricium/appengine/common/triciumtest"
)
const (
host = "https://chromium-review.googlesource.com"
)
// mockPollRestAPI allows for modification of change state returned by
// QueryChanges.
type mockPollRestAPI struct {
sync.Mutex
changes map[string][]gr.ChangeInfo
}
func (m *mockPollRestAPI) QueryChanges(c context.Context, host, project string, ts time.Time) ([]gr.ChangeInfo, bool, error) {
m.Lock()
defer m.Unlock()
if m.changes == nil {
m.changes = make(map[string][]gr.ChangeInfo)
}
id := gerritProjectID(host, project)
changes, _ := m.changes[id]
return changes, false, nil
}
func (*mockPollRestAPI) PostRobotComments(c context.Context, host, change, revision string, runID int64, comments []*track.Comment) error {
// Not used by the poller.
return nil
}
func (m *mockPollRestAPI) GetChangedLines(c context.Context, host, change, revision string) (gc.ChangedLinesInfo, error) {
// Not used by the poller.
return gc.ChangedLinesInfo{}, nil
}
func (m *mockPollRestAPI) addChanges(host, project string, c []gr.ChangeInfo) {
m.Lock()
defer m.Unlock()
if m.changes == nil {
m.changes = make(map[string][]gr.ChangeInfo)
}
id := gerritProjectID(host, project)
changes, _ := m.changes[id]
changes = append(changes, c...)
m.changes[id] = changes
}
// mockConfigProvider stores and returns canned configs.
type mockConfigProvider struct {
Projects map[string]*tricium.ProjectConfig
ServiceConfig *tricium.ServiceConfig
}
func (m *mockConfigProvider) GetServiceConfig(c context.Context) (*tricium.ServiceConfig, error) {
return m.ServiceConfig, nil
}
func (m *mockConfigProvider) GetProjectConfig(c context.Context, p string) (*tricium.ProjectConfig, error) {
pc, ok := m.Projects[p]
if !ok {
return nil, fmt.Errorf("nonexistent project")
}
return pc, nil
}
func (m *mockConfigProvider) GetAllProjectConfigs(c context.Context) (map[string]*tricium.ProjectConfig, error) {
return m.Projects, nil
}
func numEnqueuedAnalyzeRequests(ctx context.Context) int {
return len(tq.GetTestable(ctx).GetScheduledTasks()[common.AnalyzeQueue])
}
func TestPollAllProjectsBehavior(t *testing.T) {
Convey("Test Environment", t, func() {
ctx := triciumtest.Context()
cp := &mockConfigProvider{
Projects: map[string]*tricium.ProjectConfig{
"a-project": {
Repos: []*tricium.RepoDetails{
{
Source: &tricium.RepoDetails_GerritProject{
GerritProject: &tricium.GerritProject{
Host: host,
Project: "infra",
GitUrl: "https://repo-host.com/infra",
},
},
},
},
},
"b-project": {
Repos: []*tricium.RepoDetails{
{
Source: &tricium.RepoDetails_GerritProject{
GerritProject: &tricium.GerritProject{
Host: host,
Project: "project/tricium-gerrit",
GitUrl: "https://repo-host.com/playground",
},
},
},
},
},
"chromium": {
Repos: []*tricium.RepoDetails{
{
Source: &tricium.RepoDetails_GerritProject{
GerritProject: &tricium.GerritProject{
Host: host,
Project: "chromium/src",
GitUrl: "https://chromium.googlesource.com/chromium/src",
},
},
},
},
},
"chromium-m87": {
// This is a special chromium milestone project which has
// the same project config as the chromium project, but
// which should be skipped.
Repos: []*tricium.RepoDetails{
{
Source: &tricium.RepoDetails_GerritProject{
GerritProject: &tricium.GerritProject{
Host: host,
Project: "chromium/src",
GitUrl: "https://chromium.googlesource.com/chromium/src",
},
},
},
},
},
},
}
Convey("Poll puts a poll project request in the task queue for each project", func() {
So(poll(ctx, cp), ShouldBeNil)
tasks := tq.GetTestable(ctx).GetScheduledTasks()[common.PollProjectQueue]
var projects []string
for _, task := range tasks {
request := &admin.PollProjectRequest{}
So(proto.Unmarshal(task.Payload, request), ShouldBeNil)
projects = append(projects, request.Project)
}
sort.Strings(projects)
// Note that chromium-m87 is not polled.
So(projects, ShouldResemble, []string{"a-project", "b-project", "chromium"})
})
})
}
func TestPollProjectBasicBehavior(t *testing.T) {
Convey("Test Environment", t, func() {
ctx := triciumtest.Context()
now := time.Date(2017, 1, 1, 0, 0, 0, 0, time.UTC)
ctx, tc := testclock.UseTime(ctx, now)
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: identity.AnonymousIdentity,
})
cp := &mockConfigProvider{
Projects: map[string]*tricium.ProjectConfig{
"infra": {
Repos: []*tricium.RepoDetails{
{
Source: &tricium.RepoDetails_GerritProject{
GerritProject: &tricium.GerritProject{
Host: host,
Project: "infra",
GitUrl: "https://repo-host.com/infra",
},
},
},
{
Source: &tricium.RepoDetails_GerritProject{
GerritProject: &tricium.GerritProject{
Host: host,
Project: "project/tricium-gerrit",
GitUrl: "https://repo-host.com/playground",
},
},
},
},
},
},
}
projects, err := cp.GetAllProjectConfigs(ctx)
So(err, ShouldBeNil)
gerritProjects := []*tricium.GerritProject{
projects["infra"].Repos[0].GetGerritProject(),
projects["infra"].Repos[1].GetGerritProject(),
}
Convey("First poll, no changes", func() {
api := &mockPollRestAPI{}
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
Convey("Creates tracking entries for Gerrit projects", func() {
for _, gd := range gerritProjects {
p := &Project{ID: gerritProjectID(gd.Host, gd.Project)}
So(ds.Get(ctx, p), ShouldBeNil)
}
})
Convey("Does not enqueue analyze requests", func() {
So(numEnqueuedAnalyzeRequests(ctx), ShouldEqual, 0)
})
})
Convey("Second poll, no changes", func() {
api := &mockPollRestAPI{}
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
// Store last poll timestamps from first poll.
lastPolls := make(map[string]time.Time)
for _, gd := range gerritProjects {
p := &Project{ID: gerritProjectID(gd.Host, gd.Project)}
So(ds.Get(ctx, p), ShouldBeNil)
lastPolls[p.ID] = p.LastPoll
}
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
Convey("Does not update timestamp of last poll", func() {
for _, gd := range gerritProjects {
p := &Project{ID: gerritProjectID(gd.Host, gd.Project)}
So(ds.Get(ctx, p), ShouldBeNil)
t, _ := lastPolls[p.ID]
So(t.Equal(p.LastPoll), ShouldBeTrue)
}
})
Convey("Does not enqueue analyze requests", func() {
So(numEnqueuedAnalyzeRequests(ctx), ShouldEqual, 0)
})
})
Convey("First poll, with changes", func() {
api := &mockPollRestAPI{}
lastChangeTs := clock.Now(ctx)
// Fill up with one change per project.
for _, gd := range gerritProjects {
api.addChanges(gd.Host, gd.Project, []gr.ChangeInfo{
{
Project: gd.Project,
Updated: gr.TimeStamp(lastChangeTs),
Owner: &gr.AccountInfo{Email: "user@example.com"},
},
})
}
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
Convey("Does not enqueue analyze requests", func() {
So(numEnqueuedAnalyzeRequests(ctx), ShouldEqual, 0)
})
})
Convey("Second poll, with new changes adding files", func() {
api := &mockPollRestAPI{}
lastChangeTs := tc.Now().UTC()
// Fill up with one change per project.
for _, gd := range gerritProjects {
revisions := map[string]gr.RevisionInfo{
"abcdef": {
Kind: "REWORK",
Files: map[string]*gr.FileInfo{"README.md": {}},
},
}
api.addChanges(gd.Host, gd.Project, []gr.ChangeInfo{
{
ID: "project~branch~Ideadc0de",
Project: gd.Project,
Status: "NEW",
CurrentRevision: "abcdef",
Updated: gr.TimeStamp(lastChangeTs),
Revisions: revisions,
Owner: &gr.AccountInfo{Email: "user@example.com"},
},
})
}
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
tc.Add(time.Second)
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
Convey("Updates last poll timestamp to last change timestamp", func() {
for _, gd := range gerritProjects {
p := &Project{ID: gerritProjectID(gd.Host, gd.Project)}
So(ds.Get(ctx, p), ShouldBeNil)
So(lastChangeTs.Equal(p.LastPoll), ShouldBeTrue)
}
})
Convey("Enqueues analyze requests for each repo in the project", func() {
So(numEnqueuedAnalyzeRequests(ctx), ShouldEqual, len(gerritProjects))
tasks := tq.GetTestable(ctx).GetScheduledTasks()[common.AnalyzeQueue]
var projects []string
var repos []string
for _, task := range tasks {
ar := &tricium.AnalyzeRequest{}
So(proto.Unmarshal(task.Payload, ar), ShouldBeNil)
projects = append(projects, ar.Project)
repos = append(repos, ar.GetGerritRevision().GitUrl)
}
So(projects, ShouldResemble, []string{"infra", "infra"})
sort.Strings(repos)
So(repos, ShouldResemble, []string{
"https://repo-host.com/infra",
"https://repo-host.com/playground",
})
})
Convey("Adds change tracking entities", func() {
for _, gd := range gerritProjects {
So(ds.Get(ctx, &Change{
ID: "project~branch~Ideadc0de",
Parent: ds.NewKey(ctx, "GerritProject", gerritProjectID(gd.Host, gd.Project), 0, nil),
}), ShouldBeNil)
}
})
})
Convey("Poll with changes that include deleted and binary files", func() {
api := &mockPollRestAPI{}
lastChangeTs := tc.Now().UTC()
// Fill up with one change per project.
for _, gd := range gerritProjects {
revisions := map[string]gr.RevisionInfo{
"abcdef": {
Kind: "REWORK",
Files: map[string]*gr.FileInfo{
"changed.txt": {},
"deleted.txt": {Status: "D"},
"binary.png": {Binary: true},
},
},
}
api.addChanges(gd.Host, gd.Project, []gr.ChangeInfo{
{
ID: "project~branch~Ideadc0de",
Project: gd.Project,
Status: "NEW",
CurrentRevision: "abcdef",
Updated: gr.TimeStamp(lastChangeTs),
Revisions: revisions,
Owner: &gr.AccountInfo{Email: "user@example.com"},
},
})
}
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
tc.Add(time.Second)
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
Convey("Enqueued analyze requests do not include deleted files", func() {
tasks := tq.GetTestable(ctx).GetScheduledTasks()[common.AnalyzeQueue]
So(len(tasks), ShouldEqual, len(gerritProjects))
for _, task := range tasks {
ar := &tricium.AnalyzeRequest{}
err := proto.Unmarshal(task.Payload, ar)
// Sorting files according to their paths to account for random
// enumeration in go maps.
sort.Slice(ar.Files, func(i, j int) bool {
return ar.Files[i].Path < ar.Files[j].Path
})
So(err, ShouldBeNil)
So(ar.Files, ShouldResemble, []*tricium.Data_File{
{
Path: "binary.png",
IsBinary: true,
Status: tricium.Data_MODIFIED,
},
{
Path: "changed.txt",
IsBinary: false,
Status: tricium.Data_MODIFIED,
},
})
}
})
})
Convey("Poll when there is a change with no files", func() {
api := &mockPollRestAPI{}
lastChangeTs := tc.Now().UTC()
// Fill up with one change per project.
for _, gd := range gerritProjects {
revisions := map[string]gr.RevisionInfo{
"abcdef": {
Kind: "REWORK",
Files: make(map[string]*gr.FileInfo),
},
}
api.addChanges(gd.Host, gd.Project, []gr.ChangeInfo{
{
ID: "project~branch~Ideadc0de",
Project: gd.Project,
Status: "NEW",
CurrentRevision: "abcdef",
Updated: gr.TimeStamp(lastChangeTs),
Revisions: revisions,
Owner: &gr.AccountInfo{Email: "user@example.com"},
},
})
}
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
tc.Add(time.Second)
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
Convey("Does not enqueue analyze requests", func() {
So(numEnqueuedAnalyzeRequests(ctx), ShouldEqual, 0)
})
})
Convey("Poll when the current revision is has no code change.", func() {
api := &mockPollRestAPI{}
lastChangeTs := tc.Now().UTC()
// Fill up with one change per project.
for _, gd := range gerritProjects {
revisions := map[string]gr.RevisionInfo{
"abcdef": {
// Since Kind is not REWORK, the revision is considered
// "trivial", and there is no need to analyze.
Kind: "NO_CODE_CHANGE",
Files: map[string]*gr.FileInfo{
"changed.txt": {},
"binary.png": {Binary: true},
},
},
}
api.addChanges(gd.Host, gd.Project, []gr.ChangeInfo{
{
ID: "project~branch~Ideadc0de",
Project: gd.Project,
Status: "NEW",
CurrentRevision: "abcdef",
Updated: gr.TimeStamp(lastChangeTs),
Revisions: revisions,
Owner: &gr.AccountInfo{Email: "user@example.com"},
},
})
}
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
tc.Add(time.Second)
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
Convey("Does not enqueue analyze requests", func() {
So(numEnqueuedAnalyzeRequests(ctx), ShouldEqual, 0)
})
})
Convey("Poll with many changes, so paging is used", func() {
api := &mockPollRestAPI{}
// The first poll stores the timestamp.
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
tc.Add(time.Second)
// Fill up each project with multiple changes.
numChanges := 6
revBase := "abcdef"
branch := "master"
changeIDFooter := "Ideadc0de"
for _, gd := range gerritProjects {
var changes []gr.ChangeInfo
for i := 0; i < numChanges; i++ {
tc.Add(time.Second)
changeID := fmt.Sprintf("%s~%s~%s%d", gd.Project, branch, changeIDFooter, i)
rev := fmt.Sprintf("%s%d", revBase, i)
files := map[string]*gr.FileInfo{"README.md": {}}
revisions := make(map[string]gr.RevisionInfo)
revisions[rev] = gr.RevisionInfo{
Kind: "REWORK",
Files: files,
}
changes = append(changes, gr.ChangeInfo{
ID: changeID,
Project: gd.Project,
Status: "NEW",
CurrentRevision: rev,
Updated: gr.TimeStamp(tc.Now().UTC()),
Revisions: revisions,
Owner: &gr.AccountInfo{Email: "user@example.com"},
})
}
api.addChanges(gd.Host, gd.Project, changes)
}
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
Convey("Enqueues analyze requests", func() {
So(numEnqueuedAnalyzeRequests(ctx), ShouldEqual, len(gerritProjects)*numChanges)
})
Convey("Adds change tracking entities", func() {
for _, gd := range gerritProjects {
for i := 0; i < numChanges; i++ {
So(ds.Get(ctx, &Change{
ID: fmt.Sprintf("%s~%s~%s%d", gd.Project, branch, changeIDFooter, i),
Parent: ds.NewKey(ctx, "GerritProject", gerritProjectID(gd.Host, gd.Project), 0, nil),
}), ShouldBeNil)
}
}
})
})
})
}
func TestPollProjectDescriptionFlagBehavior(t *testing.T) {
// mkRevInfo generate a RevisionInfo with the given commit message.
mkRevInfo := func(commitMessage string) *gr.RevisionInfo {
return &gr.RevisionInfo{
Files: map[string]*gr.FileInfo{"README.md": {}},
Commit: &gr.CommitInfo{Message: commitMessage},
Kind: "REWORK",
}
}
// mkRevInfoMap generates a map of revisionID to RevisionInfo.
//
// It takes commitMessage as input and adds that to the "curRev" revision.
mkRevInfoMap := func(commitMessage string) map[string]gr.RevisionInfo {
return map[string]gr.RevisionInfo{
"curRev": *mkRevInfo(commitMessage),
"olderRev": {
Files: map[string]*gr.FileInfo{"another1.go": {}},
Kind: "REWORK",
},
}
}
// mkChangeInfo returns a one-item slice of ChangeInfo
// for use in the tests below, where "curRev" is the current revision.
mkChangeInfo := func(project string, lastChangeTs time.Time,
revisions map[string]gr.RevisionInfo) []gr.ChangeInfo {
return []gr.ChangeInfo{
{
ID: "project~branch~Ideadc0de",
Project: project,
Status: "NEW",
CurrentRevision: "curRev",
Updated: gr.TimeStamp(lastChangeTs),
Revisions: revisions,
Owner: &gr.AccountInfo{Email: "user@example.com"},
},
}
}
Convey("Private helper functions behave as expected", t, func() {
Convey("A summary-only message with a colon is not a footer", func() {
So(len(extractFooterFlags("Tag: something\n")), ShouldEqual, 0)
})
Convey("Footer keys are converted to title-case, values are unmodified", func() {
So(extractFooterFlags("summary\n\nkey-name: yEs\n"),
ShouldResemble, map[string]string{"Key-Name": "yEs"})
})
Convey("There can be non-flag lines in the footer paragraph", func() {
So(extractFooterFlags("summary\n\nkey-name: yEs\nnot a flag\n"),
ShouldResemble, map[string]string{"Key-Name": "yEs"})
})
Convey("http and https are not used as keys", func() {
So(extractFooterFlags("summary\n\nkey-name: yEs\nhttps://example.com\n"),
ShouldResemble, map[string]string{"Key-Name": "yEs"})
So(extractFooterFlags("summary\n\nkey-name: yEs\nhttp://example.com\n"),
ShouldResemble, map[string]string{"Key-Name": "yEs"})
// Only some URL schemas are treated specially; others are treated as keys.
So(extractFooterFlags("summary\n\nkey-name: yEs\nfoo://example.com\n"),
ShouldResemble, map[string]string{"Key-Name": "yEs", "Foo": "//example.com"})
})
Convey("Footer flags can be extracted with newline at end", func() {
So(extractFooterFlags("Summary\n\none: A\nTWO: bee\nThree: sea\n"),
ShouldResemble, map[string]string{
"One": "A",
"Two": "bee",
"Three": "sea",
})
})
Convey("Footer flags can be extracted with no newline at end", func() {
So(extractFooterFlags("Summary\n\none: A\nTWO: bee\nThree: sea"),
ShouldResemble, map[string]string{
"One": "A",
"Two": "bee",
"Three": "sea",
})
})
Convey("Commit message with no flags has no skip command", func() {
So(hasSkipCommand(&gr.RevisionInfo{
Commit: &gr.CommitInfo{Message: "one two three"},
}), ShouldBeFalse)
})
Convey("Commit message with skip flag has skip command", func() {
So(hasSkipCommand(&gr.RevisionInfo{
Commit: &gr.CommitInfo{Message: "Summary line\n\nTricium: Skip\nChange-Id: I01234\n"},
}), ShouldBeTrue)
})
Convey("no, none, skip, disable and false are all 'skip' values", func() {
So(hasSkipCommand(mkRevInfo("Summary\n\nTricium: no")), ShouldBeTrue)
So(hasSkipCommand(mkRevInfo("Summary\n\nTricium: none")), ShouldBeTrue)
So(hasSkipCommand(mkRevInfo("Summary\n\nTricium: skip")), ShouldBeTrue)
So(hasSkipCommand(mkRevInfo("Summary\n\nTricium: disable")), ShouldBeTrue)
So(hasSkipCommand(mkRevInfo("Summary\n\nTricium: false")), ShouldBeTrue)
})
Convey("Other values are not 'skip' values", func() {
So(hasSkipCommand(mkRevInfo("Summary\n\nTricium: foo")), ShouldBeFalse)
So(hasSkipCommand(mkRevInfo("Summary\n\nTricium: yes")), ShouldBeFalse)
So(hasSkipCommand(mkRevInfo("Summary\n\nTricium: affirmative")), ShouldBeFalse)
So(hasSkipCommand(mkRevInfo("Summary\n\nTricium: indeed")), ShouldBeFalse)
So(hasSkipCommand(mkRevInfo("Summary\n\nTricium: enable")), ShouldBeFalse)
})
})
Convey("Test Environment", t, func() {
ctx := triciumtest.Context()
now := time.Date(2017, 1, 1, 0, 0, 0, 0, time.UTC)
ctx, tc := testclock.UseTime(ctx, now)
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: identity.AnonymousIdentity,
})
cp := &mockConfigProvider{
Projects: map[string]*tricium.ProjectConfig{
"infra": {
Repos: []*tricium.RepoDetails{
{
Source: &tricium.RepoDetails_GerritProject{
GerritProject: &tricium.GerritProject{
Host: host,
Project: "infra",
GitUrl: "https://repo-host.com/infra",
},
},
},
{
Source: &tricium.RepoDetails_GerritProject{
GerritProject: &tricium.GerritProject{
Host: host,
Project: "project/tricium-gerrit",
GitUrl: "https://repo-host.com/playground",
},
},
},
},
},
},
}
projects, err := cp.GetAllProjectConfigs(ctx)
So(err, ShouldBeNil)
gerritProjects := []*tricium.GerritProject{
projects["infra"].Repos[0].GetGerritProject(),
projects["infra"].Repos[1].GetGerritProject(),
}
Convey("Poll when changes have Tricium: disable description flag", func() {
api := &mockPollRestAPI{}
lastChangeTs := clock.Now(ctx)
for _, gd := range gerritProjects {
api.addChanges(
gd.Host, gd.Project, mkChangeInfo(
gd.Project, lastChangeTs, mkRevInfoMap("Summary\n\nTricium: skip")))
}
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
tc.Add(time.Second)
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
Convey("No analyze requests are queued, all are skipped", func() {
So(numEnqueuedAnalyzeRequests(ctx), ShouldEqual, 0)
})
})
Convey("Poll when only one of the two changes have Tricium: disable flag", func() {
api := &mockPollRestAPI{}
lastChangeTs := clock.Now(ctx)
// Add a skipped change and non-skipped change in each project.
for _, gd := range gerritProjects {
api.addChanges(
gd.Host, gd.Project,
mkChangeInfo(gd.Project, lastChangeTs, mkRevInfoMap("Summary\n\nFoo: bar\n")))
api.addChanges(
gd.Host, gd.Project,
mkChangeInfo(gd.Project, lastChangeTs, mkRevInfoMap("Summary:\n\nTricium: disable\n")))
}
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
tc.Add(time.Second)
So(pollProject(ctx, "infra", api, cp), ShouldBeNil)
Convey("Keeps non-skipped changes, one per project", func() {
So(numEnqueuedAnalyzeRequests(ctx), ShouldEqual, len(gerritProjects))
})
})
})
}
func TestPollProjectWhitelistBehavior(t *testing.T) {
Convey("Test Environment", t, func() {
ctx := triciumtest.Context()
var (
noWhitelistProject = "no-whitelist-project"
whitelistProject = "whitelist-group-project"
whitelistGroup = "whitelist-group-name"
)
now := time.Date(2017, 1, 1, 0, 0, 0, 0, time.UTC)
ctx, tc := testclock.UseTime(ctx, now)
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: identity.AnonymousIdentity,
FakeDB: authtest.NewFakeDB(
authtest.MockMembership("user:whitelisteduser@example.com", whitelistGroup),
),
})
cp := &mockConfigProvider{
Projects: map[string]*tricium.ProjectConfig{
noWhitelistProject: {
Repos: []*tricium.RepoDetails{
{
Source: &tricium.RepoDetails_GerritProject{
GerritProject: &tricium.GerritProject{
Host: host,
Project: noWhitelistProject,
GitUrl: "https://repo-host.com/no-whitelist",
},
},
},
},
},
whitelistProject: {
Repos: []*tricium.RepoDetails{
{
Source: &tricium.RepoDetails_GerritProject{
GerritProject: &tricium.GerritProject{
Host: host,
Project: whitelistProject,
GitUrl: "https://repo-host.com/whitelist",
},
},
WhitelistedGroup: []string{whitelistGroup},
},
},
},
},
}
projects, err := cp.GetAllProjectConfigs(ctx)
So(err, ShouldBeNil)
var gerritProjects []*tricium.GerritProject
for _, pc := range projects {
for _, repo := range pc.Repos {
if gd := repo.GetGerritProject(); gd != nil {
gerritProjects = append(gerritProjects, gd)
}
}
}
Convey("No whitelisted groups means all changes are analyzed", func() {
api := &mockPollRestAPI{}
lastChangeTs := tc.Now().UTC()
revisions := map[string]gr.RevisionInfo{
"abcdef": {
Kind: "REWORK",
Files: map[string]*gr.FileInfo{"README.md": {}}},
}
api.addChanges(host, noWhitelistProject, []gr.ChangeInfo{
{
ID: "project~branch~Ideadc0de",
Project: noWhitelistProject,
Status: "NEW",
CurrentRevision: "abcdef",
Updated: gr.TimeStamp(lastChangeTs),
Revisions: revisions,
Owner: &gr.AccountInfo{Email: "whitelisteduser@example.com"},
},
})
So(pollProject(ctx, noWhitelistProject, api, cp), ShouldBeNil)
tc.Add(time.Second)
So(pollProject(ctx, noWhitelistProject, api, cp), ShouldBeNil)
Convey("Enqueues an analyze request", func() {
So(numEnqueuedAnalyzeRequests(ctx), ShouldEqual, 1)
})
})
Convey("Poll with a change by a whitelisted user", func() {
api := &mockPollRestAPI{}
lastChangeTs := tc.Now().UTC()
revisions := map[string]gr.RevisionInfo{
"abcdef": {
Kind: "REWORK",
Files: map[string]*gr.FileInfo{"README.md": {}},
},
}
api.addChanges(host, whitelistProject, []gr.ChangeInfo{
{
ID: "project~branch~Ideadc0de",
Project: whitelistProject,
Status: "NEW",
CurrentRevision: "abcdef",
Updated: gr.TimeStamp(lastChangeTs),
Revisions: revisions,
Owner: &gr.AccountInfo{Email: "whitelisteduser@example.com"},
},
})
So(pollProject(ctx, whitelistProject, api, cp), ShouldBeNil)
tc.Add(time.Second)
So(pollProject(ctx, whitelistProject, api, cp), ShouldBeNil)
Convey("Does not enqueue analyze requests", func() {
So(numEnqueuedAnalyzeRequests(ctx), ShouldEqual, 1)
})
})
Convey("Poll with a change by an unwhitelisted user", func() {
api := &mockPollRestAPI{}
lastChangeTs := tc.Now().UTC()
revisions := map[string]gr.RevisionInfo{
"abcdef": {
Files: map[string]*gr.FileInfo{"README.md": {}},
Kind: "REWORK",
},
}
api.addChanges(host, whitelistProject, []gr.ChangeInfo{
{
ID: "project~branch~Ideadc0de",
Project: whitelistProject,
Status: "NEW",
CurrentRevision: "abcdef",
Updated: gr.TimeStamp(lastChangeTs),
Revisions: revisions,
Owner: &gr.AccountInfo{Email: "somebody-else@example.com"},
},
})
So(pollProject(ctx, whitelistProject, api, cp), ShouldBeNil)
tc.Add(time.Second)
So(pollProject(ctx, whitelistProject, api, cp), ShouldBeNil)
Convey("Does not enqueue analyze requests", func() {
So(numEnqueuedAnalyzeRequests(ctx), ShouldEqual, 0)
})
})
})
}
func TestStatusCode(t *testing.T) {
ctx := triciumtest.Context()
Convey("Valid codes", t, func() {
So(statusFromCode(ctx, "A"), ShouldEqual, tricium.Data_ADDED)
So(statusFromCode(ctx, "D"), ShouldEqual, tricium.Data_DELETED)
So(statusFromCode(ctx, "R"), ShouldEqual, tricium.Data_RENAMED)
So(statusFromCode(ctx, "C"), ShouldEqual, tricium.Data_COPIED)
So(statusFromCode(ctx, "W"), ShouldEqual, tricium.Data_REWRITTEN)
So(statusFromCode(ctx, "M"), ShouldEqual, tricium.Data_MODIFIED)
})
Convey("Unknown status means modified", t, func() {
So(statusFromCode(ctx, "X"), ShouldEqual, tricium.Data_MODIFIED)
})
}