blob: 39b1cdc04d0cc33e05b726747c27b73116f8d06f [file] [log] [blame]
package autoroller
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"sync"
"testing"
"time"
assert "github.com/stretchr/testify/require"
"go.skia.org/infra/autoroll/go/autoroll_modes"
"go.skia.org/infra/autoroll/go/repo_manager"
"go.skia.org/infra/go/autoroll"
"go.skia.org/infra/go/buildbucket"
"go.skia.org/infra/go/gerrit"
"go.skia.org/infra/go/jsonutils"
"go.skia.org/infra/go/mockhttpclient"
"go.skia.org/infra/go/testutils"
)
const (
COMMITTED_STR = "Committed: https://chromium.googlesource.com/chromium/src/+/fd01dc2938"
FAKE_GERRIT_URL = "https://fake-skia-review.googlesource.com"
)
var noTrybots = []*buildbucket.Build{}
// mockRepoManager is a struct used for mocking out the AutoRoller's
// interactions with a RepoManager.
type mockRepoManager struct {
updateCount int
mockIssueNumber int64
mockFullChildHashes map[string]string
lastRollRev string
rolledPast map[string]bool
skiaHead string
sendToGerritDryRunCalled bool
sendToGerritCQCalled bool
mtx sync.RWMutex
t *testing.T
}
// Update pretends to update the mockRepoManager.
func (r *mockRepoManager) Update() error {
r.mtx.Lock()
defer r.mtx.Unlock()
if r.updateCount == 0 {
return fmt.Errorf("updateCount == 0!")
}
r.updateCount--
return nil
}
// mockUpdate increments the expected Update call count.
func (r *mockRepoManager) mockUpdate() {
r.mtx.Lock()
defer r.mtx.Unlock()
r.updateCount++
}
// assertUpdate asserts that the Update call count is zero.
func (r *mockRepoManager) assertUpdate() {
r.mtx.RLock()
defer r.mtx.RUnlock()
assert.Equal(r.t, 0, r.updateCount)
}
// getUpdateCount returns the remaining Update call count.
func (r *mockRepoManager) getUpdateCount() int {
r.mtx.RLock()
defer r.mtx.RUnlock()
return r.updateCount
}
// ChildRevList returns a list of hashes.
func (r *mockRepoManager) ChildRevList(args ...string) ([]string, error) {
r.mtx.RLock()
defer r.mtx.RUnlock()
return nil, fmt.Errorf("Not implemented")
}
// FullChildHash returns the full hash of the given short hash or ref in the
// mocked child repo.
func (r *mockRepoManager) FullChildHash(shortHash string) (string, error) {
r.mtx.RLock()
defer r.mtx.RUnlock()
h, ok := r.mockFullChildHashes[shortHash]
if !ok {
return "", fmt.Errorf("Unknown short hash: %s", shortHash)
}
return h, nil
}
// mockFullChildHash adds the given mock hash.
func (r *mockRepoManager) mockFullChildHash(short, long string) {
r.mtx.Lock()
defer r.mtx.Unlock()
r.mockFullChildHashes[short] = long
}
// LastRollRev returns the last-rolled child commit in the mocked repo.
func (r *mockRepoManager) LastRollRev() string {
r.mtx.RLock()
defer r.mtx.RUnlock()
return r.lastRollRev
}
// mockLastRollRev fakes the last roll revision.
func (r *mockRepoManager) mockLastRollRev(last string) {
r.mtx.Lock()
defer r.mtx.Unlock()
r.lastRollRev = last
}
// RolledPast determines whether DEPS has rolled past the given commit in the
// mocked repo.
func (r *mockRepoManager) RolledPast(hash string) (bool, error) {
r.mtx.RLock()
defer r.mtx.RUnlock()
rv, ok := r.rolledPast[hash]
if !ok {
r.t.Fatal(fmt.Sprintf("Unknown hash: %s", hash))
}
return rv, nil
}
// mockRolledPast pretends that the DEPS has rolled past the given commit.
func (r *mockRepoManager) mockRolledPast(hash string, rolled bool) {
r.mtx.Lock()
defer r.mtx.Unlock()
r.rolledPast[hash] = rolled
}
// NextRollRev returns the revision for the next roll.
func (r *mockRepoManager) NextRollRev() string {
r.mtx.RLock()
defer r.mtx.RUnlock()
return r.skiaHead
}
// mockNextRollRev sets the fake child origin/master branch head.
func (r *mockRepoManager) mockNextRollRev(hash string) {
r.mtx.Lock()
defer r.mtx.Unlock()
r.skiaHead = hash
}
// CreateNewRoll pretends to create a new DEPS roll from the mocked repo,
// returning the fake issue number set by the test.
func (r *mockRepoManager) CreateNewRoll(from, to string, emails []string, cqExtraTrybots string, dryRun bool) (int64, error) {
r.mtx.RLock()
defer r.mtx.RUnlock()
return r.mockIssueNumber, nil
}
// mockChildCommit pretends that a child commit has landed.
func (r *mockRepoManager) mockChildCommit(hash string) {
r.mtx.Lock()
defer r.mtx.Unlock()
if r.mockFullChildHashes == nil {
r.mockFullChildHashes = map[string]string{}
}
if r.rolledPast == nil {
r.rolledPast = map[string]bool{}
}
assert.Equal(r.t, 40, len(hash))
shortHash := hash[:12]
r.skiaHead = hash
r.mockFullChildHashes[shortHash] = hash
r.rolledPast[hash] = false
}
// rollerWillUpload sets up expectations for the roller to upload a CL. Returns
// a gerrit.ChangeInfo representing the new, in-progress DEPS roll.
func (r *mockRepoManager) rollerWillUpload(rv *mockCodereview, from, to string, tryResults []*buildbucket.Build, dryRun bool) *gerrit.ChangeInfo {
// Rietveld API only has millisecond precision.
now := time.Now().UTC().Round(time.Millisecond)
description := fmt.Sprintf(`Roll src/third_party/skia/ %s..%s (42 commits).
blah blah
TBR=some-sheriff
`, from[:12], to[:12])
r.mockIssueNumber = rv.nextIssueNum()
rev := &gerrit.Revision{
ID: "1",
Number: 1,
CreatedString: now.Format(gerrit.TIME_FORMAT),
Created: now,
}
cqLabel := gerrit.COMMITQUEUE_LABEL_SUBMIT
if dryRun {
cqLabel = gerrit.COMMITQUEUE_LABEL_DRY_RUN
}
roll := &gerrit.ChangeInfo{
Created: now,
CreatedString: now.Format(gerrit.TIME_FORMAT),
Subject: description,
ChangeId: fmt.Sprintf("%d", r.mockIssueNumber),
Issue: r.mockIssueNumber,
Labels: map[string]*gerrit.LabelEntry{
gerrit.CODEREVIEW_LABEL: {
All: []*gerrit.LabelDetail{
{
Value: gerrit.CODEREVIEW_LABEL_APPROVE,
},
},
},
gerrit.COMMITQUEUE_LABEL: {
All: []*gerrit.LabelDetail{
{
Value: cqLabel,
},
},
},
},
Owner: &gerrit.Owner{
Email: "fake-deps-roller@chromium.org",
},
Project: "skia",
Revisions: map[string]*gerrit.Revision{
"1": rev,
},
Patchsets: []*gerrit.Revision{rev},
Updated: now,
UpdatedString: now.Format(gerrit.TIME_FORMAT),
}
rv.updateIssue(roll, tryResults)
return roll
}
func (r *mockRepoManager) User() string {
return "test_user"
}
func (r *mockRepoManager) PreUploadSteps() []repo_manager.PreUploadStep {
return nil
}
func (r *mockRepoManager) SendToGerritCQ(*gerrit.ChangeInfo, string) error {
r.sendToGerritCQCalled = true
return nil
}
func (r *mockRepoManager) SendToGerritDryRun(*gerrit.ChangeInfo, string) error {
r.sendToGerritDryRunCalled = true
return nil
}
// mockCodereview is a struct used for faking responses from Rietveld.
type mockCodereview struct {
fakeIssueNum int64
g *gerrit.Gerrit
t *testing.T
urlMock *mockhttpclient.URLMock
}
// assertMocksEmpty asserts that all of the URLs in the URLMock have been used.
func (r *mockCodereview) assertMocksEmpty() {
assert.True(r.t, r.urlMock.Empty(), fmt.Sprintf("URLS:\n%s", strings.Join(r.urlMock.List(), "\n")))
}
// mockTrybotResults sets up a fake response to a request for trybot results.
func (r *mockCodereview) mockTrybotResults(issue *gerrit.ChangeInfo, results []*buildbucket.Build) {
url := fmt.Sprintf("https://cr-buildbucket.appspot.com/api/buildbucket/v1/search?tag=buildset%%3Apatch%%2Fgerrit%%2Ffake-skia-review.googlesource.com%%2F%d%%2F1", issue.Issue)
serialized, err := json.Marshal(struct {
Builds []*buildbucket.Build
}{
Builds: results,
})
assert.NoError(r.t, err)
r.urlMock.MockOnce(url, mockhttpclient.MockGetDialogue(serialized))
}
// updateIssue inserts or updates the issue in the mockCodereview.
func (r *mockCodereview) updateIssue(issue *gerrit.ChangeInfo, tryResults []*buildbucket.Build) {
url := fmt.Sprintf("%s/changes/%d/detail?o=ALL_REVISIONS", r.g.Url(0), issue.Issue)
serialized, err := json.Marshal(issue)
assert.NoError(r.t, err)
serialized = append([]byte("abcd\n"), serialized...)
r.urlMock.MockOnce(url, mockhttpclient.MockGetDialogue(serialized))
r.mockTrybotResults(issue, tryResults)
}
// modify changes the last-modified timestamp of the roll and updates it in the
// mockCodereview.
func (r *mockCodereview) modify(issue *gerrit.ChangeInfo, tryResults []*buildbucket.Build) {
now := time.Now().UTC().Round(time.Millisecond)
issue.Updated = now
issue.UpdatedString = now.Format(gerrit.TIME_FORMAT)
r.updateIssue(issue, tryResults)
}
// rollerWillCloseIssue sets expectations for the roller to close the issue.
func (r *mockCodereview) rollerWillCloseIssue(issue *gerrit.ChangeInfo) {
p := mockhttpclient.MockPostDialogue("application/json", mockhttpclient.DONT_CARE_REQUEST, []byte{})
url := fmt.Sprintf("%s/a/changes/%d/abandon", r.g.Url(0), issue.Issue)
r.urlMock.MockOnce(url, p)
}
// rollerWillSwitchDryRun sets expectations for the roller to switch the issue
// into or out of dry run mode.
func (r *mockCodereview) rollerWillSwitchDryRun(issue *gerrit.ChangeInfo, tryResults []*buildbucket.Build, dryRun bool) {
r.updateIssue(issue, tryResults) // Initial issue update.
value := gerrit.COMMITQUEUE_LABEL_SUBMIT
if dryRun {
value = gerrit.COMMITQUEUE_LABEL_DRY_RUN
}
issue.Labels[gerrit.COMMITQUEUE_LABEL].All[0].Value = value
r.updateIssue(issue, tryResults)
}
// pretendDryRunFinished sets expectations for when the dry run has finished.
func (r *mockCodereview) pretendDryRunFinished(issue *gerrit.ChangeInfo, tryResults []*buildbucket.Build, success bool) {
result := autoroll.TRYBOT_RESULT_FAILURE
if success {
result = autoroll.TRYBOT_RESULT_SUCCESS
}
for _, t := range tryResults {
t.Status = autoroll.TRYBOT_STATUS_COMPLETED
t.Result = result
}
issue.Labels[gerrit.COMMITQUEUE_LABEL].All[0].Value = gerrit.COMMITQUEUE_LABEL_NONE
r.updateIssue(issue, tryResults) // Initial issue update.
// The roller will add a comment to the issue and close it if the dry run failed.
if success {
p := mockhttpclient.MockPostDialogue("application/json", mockhttpclient.DONT_CARE_REQUEST, []byte{})
url := fmt.Sprintf("%s/a/changes/%d/revisions/%s/review", r.g.Url(0), issue.Issue, issue.Patchsets[len(issue.Patchsets)-1].ID)
r.urlMock.MockOnce(url, p)
r.updateIssue(issue, tryResults) // Update the issue after adding a comment.
} else {
r.rollerWillCloseIssue(issue)
}
}
// pretendRollFailed changes the roll to appear to have failed in the
// mockCodereview.
func (r *mockCodereview) pretendRollFailed(issue *gerrit.ChangeInfo, tryResults []*buildbucket.Build) {
issue.Labels[gerrit.COMMITQUEUE_LABEL].All[0].Value = gerrit.COMMITQUEUE_LABEL_NONE
r.modify(issue, tryResults)
}
// pretendRollLanded changes the roll to appear to have succeeded in the
// mockCodereview.
func (r *mockCodereview) pretendRollLanded(rm *mockRepoManager, issue *gerrit.ChangeInfo, tryResults []*buildbucket.Build) {
// Determine what revision we rolled to.
m := autoroll.ROLL_REV_REGEX.FindStringSubmatch(issue.Subject)
assert.NotNil(r.t, m)
assert.Equal(r.t, 3, len(m))
rolledTo, err := rm.FullChildHash(m[2])
assert.NoError(r.t, err)
rm.mockRolledPast(rolledTo, true)
rm.mockLastRollRev(rolledTo)
rm.mockUpdate()
issue.Committed = true
issue.Labels[gerrit.COMMITQUEUE_LABEL].All[0].Value = gerrit.COMMITQUEUE_LABEL_NONE
issue.Subject += "\n" + COMMITTED_STR
issue.Status = gerrit.CHANGE_STATUS_MERGED
r.modify(issue, tryResults)
}
// nextIssueNum provides auto-incrementing fake issue numbers.
func (r *mockCodereview) nextIssueNum() int64 {
n := r.fakeIssueNum
r.fakeIssueNum++
return n
}
// checkStatus verifies that we get the expected status from the roller.
func checkStatus(t *testing.T, r *AutoRoller, rv *mockCodereview, rm *mockRepoManager, expectedStatus string, current *gerrit.ChangeInfo, currentTrybots []*buildbucket.Build, currentDryRun bool, last *gerrit.ChangeInfo, lastTrybots []*buildbucket.Build, lastDryRun bool) {
rv.assertMocksEmpty()
rm.assertUpdate()
s := r.GetStatus(true)
assert.Equal(t, expectedStatus, s.Status)
assert.Equal(t, s.Error, "")
checkRoll := func(t *testing.T, expect *gerrit.ChangeInfo, actual *autoroll.AutoRollIssue, expectTrybots []*buildbucket.Build, dryRun bool) {
if expect != nil {
assert.NotNil(t, actual)
ari, err := autoroll.FromGerritChangeInfo(expect, rm.FullChildHash, false)
assert.NoError(t, err)
tryResults := make([]*autoroll.TryResult, 0, len(expectTrybots))
for _, b := range expectTrybots {
tryResult, err := autoroll.TryResultFromBuildbucket(b)
assert.NoError(t, err)
tryResults = append(tryResults, tryResult)
}
ari.TryResults = tryResults
// This is kind of a hack to prevent having to pass the
// expected dry run result around.
if dryRun {
if ari.AllTrybotsFinished() {
ari.Result = autoroll.ROLL_RESULT_DRY_RUN_FAILURE
if ari.AllTrybotsSucceeded() {
ari.Result = autoroll.ROLL_RESULT_DRY_RUN_SUCCESS
}
}
}
assert.NoError(t, ari.Validate())
testutils.AssertDeepEqual(t, ari, actual)
} else {
assert.Nil(t, actual)
}
}
checkRoll(t, current, s.CurrentRoll, currentTrybots, currentDryRun)
checkRoll(t, last, s.LastRoll, lastTrybots, lastDryRun)
}
// setup initializes a fake AutoRoller for testing. It returns the working
// directory, AutoRoller instance, URLMock for faking HTTP requests, and an
// gerrit.ChangeInfo representing the first CL that was uploaded by the AutoRoller.
func setup(t *testing.T, strategy repo_manager.NextRollStrategy) (string, *AutoRoller, *mockRepoManager, *mockCodereview, *gerrit.ChangeInfo) {
testutils.SkipIfShort(t)
// Setup mocks.
urlMock := mockhttpclient.NewURLMock()
workdir, err := ioutil.TempDir("", "test_autoroll_mode_")
assert.NoError(t, err)
gitcookies := path.Join(workdir, "gitcookies_fake")
assert.NoError(t, ioutil.WriteFile(gitcookies, []byte(".googlesource.com\tTRUE\t/\tTRUE\t123\to\tgit-user.google.com=abc123"), os.ModePerm))
g, err := gerrit.NewGerrit(FAKE_GERRIT_URL, gitcookies, urlMock.Client())
assert.NoError(t, err)
rv := &mockCodereview{
fakeIssueNum: 10001,
g: g,
t: t,
urlMock: urlMock,
}
rm := &mockRepoManager{t: t}
repo_manager.NewDEPSRepoManager = func(workdir, parentRepo, parentBranch, childPath, childBranch string, depot_tools string, g *gerrit.Gerrit, strategy repo_manager.NextRollStrategy, preUploadSteps []string) (repo_manager.RepoManager, error) {
return rm, nil
}
// Set up more test data.
initialCommit := "abc1231010101010101010101010101010101010"
rm.mockChildCommit(initialCommit)
rm.mockChildCommit("def4561010101010101010101010101010101010")
rm.mockLastRollRev(initialCommit)
rm.mockRolledPast(initialCommit, true)
roll1 := rm.rollerWillUpload(rv, rm.LastRollRev(), rm.NextRollRev(), noTrybots, false)
// Create the roller.
roller, err := NewAutoRoller(workdir, "parent.git", "master", "src/third_party/skia", "master", "", []string{}, g, "depot_tools", false, false, strategy)
assert.NoError(t, err)
// Verify that the bot ran successfully.
checkStatus(t, roller, rv, rm, STATUS_IN_PROGRESS, roll1, noTrybots, false, nil, nil, false)
return workdir, roller, rm, rv, roll1
}
// TestAutoRollBasic ensures that the typical function of the AutoRoller works
// as expected.
func TestAutoRollBasic(t *testing.T) {
testutils.LargeTest(t)
// setup will initialize the roller and upload a CL.
s, err := repo_manager.GetNextRollStrategy(repo_manager.ROLL_STRATEGY_BATCH, "master", "")
assert.NoError(t, err)
workdir, roller, rm, rv, roll1 := setup(t, s)
defer func() {
assert.NoError(t, roller.Close())
assert.NoError(t, os.RemoveAll(workdir))
}()
// Run again. Verify that we let the currently-running roll keep going.
rv.updateIssue(roll1, noTrybots)
assert.NoError(t, roller.doAutoRoll())
checkStatus(t, roller, rv, rm, STATUS_IN_PROGRESS, roll1, noTrybots, false, nil, nil, false)
// The roll failed. Verify that we close it and upload another one.
rv.pretendRollFailed(roll1, noTrybots)
rv.rollerWillCloseIssue(roll1)
roll2 := rm.rollerWillUpload(rv, rm.LastRollRev(), rm.NextRollRev(), noTrybots, false)
assert.NoError(t, roller.doAutoRoll())
// The roller should have closed this CL.
roll1.Status = gerrit.CHANGE_STATUS_ABANDONED
checkStatus(t, roller, rv, rm, STATUS_IN_PROGRESS, roll2, noTrybots, false, roll1, noTrybots, false)
// The second roll succeeded. Verify that we're up-to-date.
rv.pretendRollLanded(rm, roll2, noTrybots)
assert.NoError(t, roller.doAutoRoll())
checkStatus(t, roller, rv, rm, STATUS_UP_TO_DATE, nil, nil, false, roll2, noTrybots, false)
// Verify that we remain idle.
assert.NoError(t, roller.doAutoRoll())
checkStatus(t, roller, rv, rm, STATUS_UP_TO_DATE, nil, nil, false, roll2, noTrybots, false)
}
// TestAutoRollStop ensures that we can properly stop and restart the
// AutoRoller.
func TestAutoRollStop(t *testing.T) {
testutils.MediumTest(t)
// setup will initialize the roller and upload a CL.
s, err := repo_manager.GetNextRollStrategy(repo_manager.ROLL_STRATEGY_BATCH, "master", "")
assert.NoError(t, err)
workdir, roller, rm, rv, roll1 := setup(t, s)
defer func() {
assert.NoError(t, roller.Close())
assert.NoError(t, os.RemoveAll(workdir))
}()
// Stop the bot. Ensure that we close the in-progress roll and don't upload a new one.
rv.updateIssue(roll1, noTrybots)
rv.rollerWillCloseIssue(roll1)
// After the roller closes the CL, it will grab its info from Rietveld
// and expect the CQ bit to be unset. and the issue to be closed.
roll1.Status = gerrit.CHANGE_STATUS_ABANDONED
roll1.Labels[gerrit.COMMITQUEUE_LABEL].All[0].Value = gerrit.COMMITQUEUE_LABEL_NONE
// Change the mode, run the bot.
u := "test@google.com"
assert.NoError(t, roller.SetMode(autoroll_modes.MODE_STOPPED, u, "Stoppit!"))
// The roller should have closed the CL.
roll1.Status = gerrit.CHANGE_STATUS_ABANDONED
roll1.Labels[gerrit.COMMITQUEUE_LABEL].All[0].Value = gerrit.COMMITQUEUE_LABEL_NONE
checkStatus(t, roller, rv, rm, STATUS_STOPPED, nil, nil, false, roll1, noTrybots, false)
// Ensure that we don't upload another CL now that we're stopped.
assert.NoError(t, roller.doAutoRoll())
checkStatus(t, roller, rv, rm, STATUS_STOPPED, nil, nil, false, roll1, noTrybots, false)
// Resume the bot. Ensure that we upload a new CL.
roll2 := rm.rollerWillUpload(rv, rm.LastRollRev(), rm.NextRollRev(), noTrybots, false)
assert.NoError(t, roller.SetMode(autoroll_modes.MODE_RUNNING, u, "Resume!"))
checkStatus(t, roller, rv, rm, STATUS_IN_PROGRESS, roll2, noTrybots, false, roll1, noTrybots, false)
// Pretend the roll landed.
rv.pretendRollLanded(rm, roll2, noTrybots)
assert.NoError(t, roller.doAutoRoll())
checkStatus(t, roller, rv, rm, STATUS_UP_TO_DATE, nil, nil, false, roll2, noTrybots, false)
// Stop the roller again.
rm.mockChildCommit("adbcdf1010101010101010101010101010101010")
assert.NoError(t, roller.SetMode(autoroll_modes.MODE_STOPPED, u, "Stop!"))
checkStatus(t, roller, rv, rm, STATUS_STOPPED, nil, nil, false, roll2, noTrybots, false)
// Ensure that we don't upload another CL now that we're stopped.
assert.NoError(t, roller.doAutoRoll())
checkStatus(t, roller, rv, rm, STATUS_STOPPED, nil, nil, false, roll2, noTrybots, false)
}
// TestAutoRollDryRun ensures that the Dry Run functionalify works as expected.
func TestAutoRollDryRun(t *testing.T) {
testutils.MediumTest(t)
s, err := repo_manager.GetNextRollStrategy(repo_manager.ROLL_STRATEGY_BATCH, "master", "")
assert.NoError(t, err)
workdir, roller, rm, rv, roll1 := setup(t, s)
defer func() {
assert.NoError(t, roller.Close())
assert.NoError(t, os.RemoveAll(workdir))
}()
// Change the mode to dry run. Expect the bot to switch the in-progress
// roll to a dry run. There is one unfinished trybot.
u := "test@google.com"
trybot := &buildbucket.Build{
Created: jsonutils.Time(time.Now().UTC().Round(time.Millisecond)),
Status: autoroll.TRYBOT_STATUS_STARTED,
ParametersJson: "{\"builder_name\":\"fake-builder\",\"properties\":{\"category\":\"cq\"}}",
}
trybots := []*buildbucket.Build{trybot}
rv.rollerWillSwitchDryRun(roll1, trybots, true)
assert.NoError(t, roller.SetMode(autoroll_modes.MODE_DRY_RUN, u, "Dry run."))
assert.True(t, rm.sendToGerritDryRunCalled)
checkStatus(t, roller, rv, rm, STATUS_DRY_RUN_IN_PROGRESS, roll1, trybots, true, nil, nil, false)
// Dry run succeeded.
rv.pretendDryRunFinished(roll1, trybots, true)
assert.NoError(t, roller.doAutoRoll())
checkStatus(t, roller, rv, rm, STATUS_DRY_RUN_SUCCESS, roll1, trybots, true, nil, nil, false)
// Run again. Ensure that we don't do anything crazy.
rv.updateIssue(roll1, trybots)
assert.NoError(t, roller.doAutoRoll())
checkStatus(t, roller, rv, rm, STATUS_DRY_RUN_SUCCESS, roll1, trybots, true, nil, nil, false)
// Add an experimental trybot. Ensure that its failure is ignored.
trybots = append(trybots, &buildbucket.Build{
Created: jsonutils.Time(time.Now().UTC().Round(time.Millisecond)),
Result: autoroll.TRYBOT_RESULT_FAILURE,
Status: autoroll.TRYBOT_STATUS_COMPLETED,
ParametersJson: "{\"builder_name\":\"fake-builder\",\"properties\":{\"category\":\"cq-experimental\"}}",
})
rv.updateIssue(roll1, trybots)
assert.NoError(t, roller.doAutoRoll())
checkStatus(t, roller, rv, rm, STATUS_DRY_RUN_SUCCESS, roll1, trybots, true, nil, nil, false)
// New child commit: verify that we close the existing dry run and open another.
rm.mockChildCommit("adbcdf1010101010101010101010101010101010")
rv.updateIssue(roll1, trybots)
rv.rollerWillCloseIssue(roll1)
trybot2 := &buildbucket.Build{
Created: jsonutils.Time(time.Now().UTC().Round(time.Millisecond)),
Status: autoroll.TRYBOT_STATUS_STARTED,
ParametersJson: "{\"builder_name\":\"fake-builder\",\"properties\":{\"category\":\"cq\"}}",
}
trybots2 := []*buildbucket.Build{trybot2}
roll2 := rm.rollerWillUpload(rv, rm.LastRollRev(), rm.NextRollRev(), trybots2, true)
assert.NoError(t, roller.doAutoRoll())
// Roller should have closed this issue.
roll1.Status = gerrit.CHANGE_STATUS_ABANDONED
checkStatus(t, roller, rv, rm, STATUS_DRY_RUN_IN_PROGRESS, roll2, trybots2, true, roll1, trybots, true)
// Dry run failed. Ensure that we close the roll and open another, same
// as in non-dry-run mode.
rv.pretendDryRunFinished(roll2, trybots2, false)
trybot3 := &buildbucket.Build{
Created: jsonutils.Time(time.Now().UTC().Round(time.Millisecond)),
Status: autoroll.TRYBOT_STATUS_STARTED,
ParametersJson: "{\"builder_name\":\"fake-builder\",\"properties\":{\"category\":\"cq\"}}",
}
trybots3 := []*buildbucket.Build{trybot3}
roll3 := rm.rollerWillUpload(rv, rm.LastRollRev(), rm.NextRollRev(), trybots3, true)
assert.NoError(t, roller.doAutoRoll())
// Roller should have closed this issue.
roll2.Status = gerrit.CHANGE_STATUS_ABANDONED
checkStatus(t, roller, rv, rm, STATUS_DRY_RUN_IN_PROGRESS, roll3, trybots3, true, roll2, trybots2, true)
// Ensure that we switch back to normal running mode as expected.
rv.rollerWillSwitchDryRun(roll3, trybots3, false)
assert.NoError(t, roller.SetMode(autoroll_modes.MODE_RUNNING, u, "Normal mode."))
checkStatus(t, roller, rv, rm, STATUS_IN_PROGRESS, roll3, trybots3, false, roll2, trybots2, true)
// Switch back to dry run.
rv.rollerWillSwitchDryRun(roll3, trybots3, true)
assert.NoError(t, roller.SetMode(autoroll_modes.MODE_DRY_RUN, u, "Dry run again."))
checkStatus(t, roller, rv, rm, STATUS_DRY_RUN_IN_PROGRESS, roll3, trybots3, true, roll2, trybots2, true)
// Dry run succeeded.
rv.pretendDryRunFinished(roll3, trybots3, true)
assert.NoError(t, roller.doAutoRoll())
checkStatus(t, roller, rv, rm, STATUS_DRY_RUN_SUCCESS, roll3, trybots3, true, roll2, trybots2, true)
// The successful dry run will not have the commit bit set. Make sure
// that, when we switch back into normal mode, we re-set the commit bit
// instead of closing the roll and opening a new one.
rv.rollerWillSwitchDryRun(roll3, trybots3, false)
assert.NoError(t, roller.SetMode(autoroll_modes.MODE_RUNNING, u, "Normal mode."))
checkStatus(t, roller, rv, rm, STATUS_IN_PROGRESS, roll3, trybots3, false, roll2, trybots2, true)
}
// TestAutoRollCommitLandRace ensures that we correctly handle the case in which
// a roll CL succeeds, is closed by the CQ, but does not show up in the repo by
// the time we check for it. In this case, we expect the roller to repeatedly
// sync the code, waiting for the commit to show up.
func TestAutoRollCommitLandRace(t *testing.T) {
testutils.LargeTest(t)
s, err := repo_manager.GetNextRollStrategy(repo_manager.ROLL_STRATEGY_BATCH, "master", "")
assert.NoError(t, err)
workdir, roller, rm, rv, roll1 := setup(t, s)
defer func() {
assert.NoError(t, roller.Close())
assert.NoError(t, os.RemoveAll(workdir))
}()
// Pretend the roll landed but has not yet showed up in the repo.
trybot := &buildbucket.Build{
Created: jsonutils.Time(time.Now().UTC().Round(time.Millisecond)),
Status: autoroll.TRYBOT_STATUS_COMPLETED,
Result: autoroll.TRYBOT_RESULT_SUCCESS,
ParametersJson: "{\"builder_name\":\"fake-builder\",\"properties\":{\"category\":\"cq\"}}",
}
trybots := []*buildbucket.Build{trybot}
roll1.Committed = true
roll1.Status = gerrit.CHANGE_STATUS_MERGED
roll1.Labels[gerrit.COMMITQUEUE_LABEL].All[0].Value = gerrit.COMMITQUEUE_LABEL_NONE
roll1.Subject += "\n" + COMMITTED_STR
rv.modify(roll1, trybots)
// The repo will have to force update multiple times.
rm.mockUpdate()
rm.mockUpdate()
rm.mockUpdate()
// This goroutine will cause the CL to "land" after a couple of tries.
go func() {
for {
if rm.getUpdateCount() == 0 {
m := autoroll.ROLL_REV_REGEX.FindStringSubmatch(roll1.Subject)
assert.NotNil(t, m)
assert.Equal(t, 3, len(m))
rolledTo, err := rm.FullChildHash(m[2])
assert.NoError(t, err)
rm.mockRolledPast(rolledTo, true)
rm.mockLastRollRev(rolledTo)
rm.mockUpdate()
return
}
time.Sleep(time.Second)
}
}()
// Run the roller.
assert.NoError(t, roller.doAutoRoll())
checkStatus(t, roller, rv, rm, STATUS_UP_TO_DATE, nil, nil, false, roll1, trybots, false)
}
// TestAutoRollThrottle ensures that we properly throttle the roller so that it
// doesn't upload new CLs over and over.
func TestAutoRollThrottle(t *testing.T) {
testutils.MediumTest(t)
s, err := repo_manager.GetNextRollStrategy(repo_manager.ROLL_STRATEGY_BATCH, "master", "")
assert.NoError(t, err)
workdir, roller, rm, rv, roll1 := setup(t, s)
defer func() {
assert.NoError(t, roller.Close())
assert.NoError(t, os.RemoveAll(workdir))
}()
// The roll failed. Verify that we close it and upload another one.
rv.pretendRollFailed(roll1, noTrybots)
rv.rollerWillCloseIssue(roll1)
roll2 := rm.rollerWillUpload(rv, rm.LastRollRev(), rm.NextRollRev(), noTrybots, false)
assert.NoError(t, roller.doAutoRoll())
// The roller should have closed this CL.
roll1.Status = gerrit.CHANGE_STATUS_ABANDONED
checkStatus(t, roller, rv, rm, STATUS_IN_PROGRESS, roll2, noTrybots, false, roll1, noTrybots, false)
// The roll failed. Verify that we close it and upload another one.
rv.pretendRollFailed(roll2, noTrybots)
rv.rollerWillCloseIssue(roll2)
roll3 := rm.rollerWillUpload(rv, rm.LastRollRev(), rm.NextRollRev(), noTrybots, false)
assert.NoError(t, roller.doAutoRoll())
// The roller should have closed this CL.
roll2.Status = gerrit.CHANGE_STATUS_ABANDONED
checkStatus(t, roller, rv, rm, STATUS_IN_PROGRESS, roll3, noTrybots, false, roll2, noTrybots, false)
// Now we should be throttled.
rv.pretendRollFailed(roll3, noTrybots)
rv.rollerWillCloseIssue(roll3)
assert.NoError(t, roller.doAutoRoll())
// The roller should have closed this CL.
roll3.Status = gerrit.CHANGE_STATUS_ABANDONED
checkStatus(t, roller, rv, rm, STATUS_THROTTLED, nil, nil, false, roll3, noTrybots, false)
}
// TestAutoRollSingle ensures that the one-at-a-time mode works as expected.
// This is more of a sanity check, since the actual behavior is done in the
// RepoManager.
func TestAutoRollSingle(t *testing.T) {
testutils.MediumTest(t)
// setup will initialize the roller and upload a CL.
s, err := repo_manager.GetNextRollStrategy(repo_manager.ROLL_STRATEGY_BATCH, "master", "")
assert.NoError(t, err)
workdir, roller, rm, rv, roll1 := setup(t, s)
defer func() {
assert.NoError(t, roller.Close())
assert.NoError(t, os.RemoveAll(workdir))
}()
c2 := "1111111111111111111111111111111111111111"
c3 := "2222222222222222222222222222222222222222"
rm.mockChildCommit(c2)
rm.mockChildCommit(c3)
fullHash := func(c string) (string, error) {
if strings.HasPrefix(c2, c) {
return c2, nil
}
if strings.HasPrefix(c3, c) {
return c3, nil
}
return c, nil
}
check := func(i *gerrit.ChangeInfo, c string) {
ari, err := autoroll.FromGerritChangeInfo(i, fullHash, false)
assert.NoError(t, err)
assert.Equal(t, c, ari.RollingTo)
}
// Verify that roll1 has a single commit.
check(roll1, "def456101010") // From setup()
// The roll landed, but we still have commits to roll.
rv.pretendRollLanded(rm, roll1, noTrybots)
roll2 := rm.rollerWillUpload(rv, rm.LastRollRev(), c2, noTrybots, false)
assert.NoError(t, roller.doAutoRoll())
checkStatus(t, roller, rv, rm, STATUS_IN_PROGRESS, roll2, noTrybots, false, roll1, noTrybots, false)
check(roll2, c2)
// Land, upload, repeat until up-to-date.
rv.pretendRollLanded(rm, roll2, noTrybots)
roll3 := rm.rollerWillUpload(rv, rm.LastRollRev(), c3, noTrybots, false)
assert.NoError(t, roller.doAutoRoll())
checkStatus(t, roller, rv, rm, STATUS_IN_PROGRESS, roll3, noTrybots, false, roll2, noTrybots, false)
check(roll3, c3)
// The roll succeeded. Verify that we're up-to-date.
rv.pretendRollLanded(rm, roll3, noTrybots)
assert.NoError(t, roller.doAutoRoll())
checkStatus(t, roller, rv, rm, STATUS_UP_TO_DATE, nil, nil, false, roll3, noTrybots, false)
}