blob: 9f4b932bd54d1d42312ac7078e9aa3cdb8c0fa42 [file] [log] [blame]
package repo_manager
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"os"
"path"
"strings"
"testing"
"github.com/stretchr/testify/require"
"go.skia.org/infra/autoroll/go/codereview"
"go.skia.org/infra/go/gerrit"
"go.skia.org/infra/go/git"
git_testutils "go.skia.org/infra/go/git/testutils"
gitiles_testutils "go.skia.org/infra/go/gitiles/testutils"
"go.skia.org/infra/go/mockhttpclient"
"go.skia.org/infra/go/testutils"
"go.skia.org/infra/go/testutils/unittest"
)
const (
afdoRevPrev = "chromeos-chrome-amd64-66.0.3336.0_rc-r0-merged.afdo.bz2"
afdoRevBase = "chromeos-chrome-amd64-66.0.3336.0_rc-r1-merged.afdo.bz2"
afdoRevNext = "chromeos-chrome-amd64-66.0.3337.0_rc-r1-merged.afdo.bz2"
afdoTimePrev = "2009-11-10T23:00:00Z"
afdoTimeBase = "2009-11-10T23:01:00Z"
afdoTimeNext = "2009-11-10T23:02:00Z"
AFDO_GS_BUCKET = "chromeos-prebuilt"
AFDO_GS_PATH = "afdo-job/llvm"
// Example name: chromeos-chrome-amd64-63.0.3239.57_rc-r1.afdo.bz2
AFDO_VERSION_REGEX = ("^chromeos-chrome-amd64-" + // Prefix
"(\\d+)\\.(\\d+)\\.(\\d+)\\.0" + // Version
"_rc-r(\\d+)" + // Revision
"-merged\\.afdo\\.bz2$") // Suffix
AFDO_SHORT_REV_REGEX = "(\\d+)\\.(\\d+)\\.(\\d+)\\.0_rc-r(\\d+)-merged"
AFDO_VERSION_FILE_PATH = "chrome/android/profiles/newest.txt"
TMPL_COMMIT_MSG_AFDO = `Roll AFDO from {{.RollingFrom.String}} to {{.RollingTo.String}}
This CL may cause a small binary size increase, roughly proportional
to how long it's been since our last AFDO profile roll. For larger
increases (around or exceeding 100KB), please file a bug against
gbiv@chromium.org. Additional context: https://crbug.com/805539
Please note that, despite rolling to chrome/android, this profile is
used for both Linux and Android.
If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
{{.ServerURL}}
Please CC {{stringsJoin .Reviewers ","}} on the revert to ensure that a human
is aware of the problem.
To report a problem with the AutoRoller itself, please file a bug:
https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug
Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+/master/autoroll/README.md
Tbr: {{stringsJoin .Reviewers ","}}
`
)
func TestCompareSemanticVersions(t *testing.T) {
unittest.SmallTest(t)
test := func(a, b []int, expect int) {
require.Equal(t, expect, compareSemanticVersions(a, b))
}
test([]int{}, []int{}, 0)
test([]int{}, []int{1}, 1)
test([]int{1}, []int{}, -1)
test([]int{1}, []int{1}, 0)
test([]int{0}, []int{1}, 1)
test([]int{1}, []int{0}, -1)
test([]int{1, 1}, []int{1, 0}, -1)
test([]int{1}, []int{1, 0}, 1)
test([]int{1, 0}, []int{1}, -1)
}
func afdoCfg() *SemVerGCSRepoManagerConfig {
return &SemVerGCSRepoManagerConfig{
GCSRepoManagerConfig: GCSRepoManagerConfig{
NoCheckoutRepoManagerConfig: NoCheckoutRepoManagerConfig{
CommonRepoManagerConfig: CommonRepoManagerConfig{
ChildBranch: "master",
ChildPath: "unused/by/afdo/repomanager",
CommitMsgTmpl: TMPL_COMMIT_MSG_AFDO,
ParentBranch: "master",
},
ParentRepo: "", // Filled in after GitInit().
},
GCSBucket: "chromeos-prebuilt",
GCSPath: "afdo-job/llvm",
VersionFile: "chrome/android/profiles/newest.txt",
},
ShortRevRegex: "(\\d+)\\.(\\d+)\\.(\\d+)\\.0_rc-r(\\d+)-merged",
VersionRegex: "^chromeos-chrome-amd64-(\\d+)\\.(\\d+)\\.(\\d+)\\.0_rc-r(\\d+)-merged\\.afdo\\.bz2$",
}
}
func gerritCR(t *testing.T, g gerrit.GerritInterface) codereview.CodeReview {
rv, err := (&codereview.GerritConfig{
URL: "https://skia-review.googlesource.com",
Project: "skia",
Config: codereview.GERRIT_CONFIG_CHROMIUM,
}).Init(g, nil)
require.NoError(t, err)
return rv
}
func setupAfdo(t *testing.T) (context.Context, *gcsRepoManager, *mockhttpclient.URLMock, *gitiles_testutils.MockRepo, *git_testutils.GitBuilder, func()) {
wd, err := ioutil.TempDir("", "")
require.NoError(t, err)
ctx := context.Background()
// Create child and parent repos.
parent := git_testutils.GitInit(t, ctx)
parent.Add(context.Background(), AFDO_VERSION_FILE_PATH, afdoRevBase)
parent.Commit(context.Background())
urlmock := mockhttpclient.NewURLMock()
mockParent := gitiles_testutils.NewMockRepo(t, parent.RepoUrl(), git.GitDir(parent.Dir()), urlmock)
gUrl := "https://fake-skia-review.googlesource.com"
gitcookies := path.Join(wd, "gitcookies_fake")
require.NoError(t, ioutil.WriteFile(gitcookies, []byte(".googlesource.com\tTRUE\t/\tTRUE\t123\to\tgit-user.google.com=abc123"), os.ModePerm))
serialized, err := json.Marshal(&gerrit.AccountDetails{
AccountId: 101,
Name: mockUser,
Email: mockUser,
UserName: mockUser,
})
require.NoError(t, err)
serialized = append([]byte("abcd\n"), serialized...)
urlmock.MockOnce(gUrl+"/a/accounts/self/detail", mockhttpclient.MockGetDialogue(serialized))
g, err := gerrit.NewGerrit(gUrl, gitcookies, urlmock.Client())
require.NoError(t, err)
cfg := afdoCfg()
cfg.ParentRepo = parent.RepoUrl()
// Initial update. Everything up-to-date.
mockParent.MockGetCommit(ctx, "master")
parentMaster, err := git.GitDir(parent.Dir()).RevParse(ctx, "HEAD")
require.NoError(t, err)
mockParent.MockReadFile(ctx, AFDO_VERSION_FILE_PATH, parentMaster)
mockGSList(t, urlmock, AFDO_GS_BUCKET, AFDO_GS_PATH, map[string]string{
afdoRevBase: afdoTimeBase,
})
rm, err := NewSemVerGCSRepoManager(ctx, cfg, wd, g, "fake.server.com", urlmock.Client(), gerritCR(t, g), false)
require.NoError(t, err)
_, _, _, err = rm.Update(ctx)
require.NoError(t, err)
cleanup := func() {
testutils.RemoveAll(t, wd)
parent.Cleanup()
}
return ctx, rm.(*gcsRepoManager), urlmock, mockParent, parent, cleanup
}
type gsObject struct {
Kind string `json:"kind"`
Id string `json:"id"`
SelfLink string `json:"selfLink"`
Name string `json:"name"`
Bucket string `json:"bucket"`
Generation string `json:"generation"`
Metageneration string `json:"metageneration"`
ContentType string `json:"contentType"`
TimeCreated string `json:"timeCreated"`
Updated string `json:"updated"`
StorageClass string `json:"storageClass"`
TimeStorageClassUpdated string `json:"timeStorageClassUpdated"`
Size string `json:"size"`
Md5Hash string `json:"md5Hash"`
MediaLink string `json:"mediaLink"`
Crc32c string `json:"crc32c"`
Etag string `json:"etag"`
}
type gsObjectList struct {
Kind string `json:"kind"`
Items []gsObject `json:"items"`
}
func mockGSList(t *testing.T, urlmock *mockhttpclient.URLMock, bucket, gsPath string, items map[string]string) {
fakeUrl := fmt.Sprintf("https://www.googleapis.com/storage/v1/b/%s/o?alt=json&delimiter=&pageToken=&prefix=%s&prettyPrint=false&projection=full&versions=false", bucket, url.PathEscape(gsPath))
resp := gsObjectList{
Kind: "storage#objects",
Items: []gsObject{},
}
for item, timestamp := range items {
resp.Items = append(resp.Items, gsObject{
Kind: "storage#object",
Id: path.Join(bucket+gsPath, item),
SelfLink: path.Join(bucket+gsPath, item),
Name: item,
Bucket: bucket,
Generation: "1",
Metageneration: "1",
ContentType: "application/octet-stream",
TimeCreated: timestamp,
Updated: timestamp,
StorageClass: "MULTI_REGIONAL",
TimeStorageClassUpdated: timestamp,
Size: "12345",
Md5Hash: "dsafkldkldsaf",
MediaLink: fakeUrl + item,
Crc32c: "eiekls",
Etag: "lasdfklds",
})
}
respBytes, err := json.MarshalIndent(resp, "", " ")
require.NoError(t, err)
urlmock.MockOnce(fakeUrl, mockhttpclient.MockGetDialogue(respBytes))
}
func mockGSObject(t *testing.T, urlmock *mockhttpclient.URLMock, bucket, gsPath, item, timestamp string) {
fakeUrl := fmt.Sprintf("https://www.googleapis.com/storage/v1/b/%s/o/%s?alt=json&prettyPrint=false&projection=full", bucket, url.PathEscape(path.Join(gsPath, item)))
resp := gsObject{
Kind: "storage#object",
Id: path.Join(bucket+gsPath, item),
SelfLink: path.Join(bucket+gsPath, item),
Name: item,
Bucket: bucket,
Generation: "1",
Metageneration: "1",
ContentType: "application/octet-stream",
TimeCreated: timestamp,
Updated: timestamp,
StorageClass: "MULTI_REGIONAL",
TimeStorageClassUpdated: timestamp,
Size: "12345",
Md5Hash: "dsafkldkldsaf",
MediaLink: fakeUrl,
Crc32c: "eiekls",
Etag: "lasdfklds",
}
respBytes, err := json.MarshalIndent(resp, "", " ")
require.NoError(t, err)
urlmock.MockOnce(fakeUrl, mockhttpclient.MockGetDialogue(respBytes))
}
func TestAFDORepoManager(t *testing.T) {
unittest.LargeTest(t)
ctx, rm, urlmock, mockParent, parent, cleanup := setupAfdo(t)
defer cleanup()
mockParent.MockGetCommit(ctx, "master")
parentMaster, err := git.GitDir(parent.Dir()).RevParse(ctx, "HEAD")
require.NoError(t, err)
mockParent.MockReadFile(ctx, AFDO_VERSION_FILE_PATH, parentMaster)
mockGSList(t, urlmock, AFDO_GS_BUCKET, AFDO_GS_PATH, map[string]string{
afdoRevBase: afdoTimeBase,
})
lastRollRev, tipRev, notRolledRevs, err := rm.Update(ctx)
require.NoError(t, err)
require.Equal(t, afdoRevBase, lastRollRev.Id)
require.Equal(t, afdoRevBase, tipRev.Id)
mockGSObject(t, urlmock, AFDO_GS_BUCKET, AFDO_GS_PATH, afdoRevPrev, afdoTimePrev)
prev, err := rm.GetRevision(ctx, afdoRevPrev)
require.NoError(t, err)
require.Equal(t, afdoRevPrev, prev.Id)
mockGSObject(t, urlmock, AFDO_GS_BUCKET, AFDO_GS_PATH, afdoRevBase, afdoTimeBase)
base, err := rm.GetRevision(ctx, afdoRevBase)
require.NoError(t, err)
require.Equal(t, afdoRevBase, base.Id)
mockGSObject(t, urlmock, AFDO_GS_BUCKET, AFDO_GS_PATH, afdoRevNext, afdoTimeNext)
next, err := rm.GetRevision(ctx, afdoRevNext)
require.NoError(t, err)
require.Equal(t, afdoRevNext, next.Id)
require.Empty(t, rm.preUploadSteps)
require.Equal(t, 0, len(notRolledRevs))
// There's a new version.
mockParent.MockGetCommit(ctx, "master")
mockParent.MockReadFile(ctx, AFDO_VERSION_FILE_PATH, parentMaster)
mockGSList(t, urlmock, AFDO_GS_BUCKET, AFDO_GS_PATH, map[string]string{
afdoRevBase: afdoTimeBase,
afdoRevNext: afdoTimeNext,
})
lastRollRev, tipRev, notRolledRevs, err = rm.Update(ctx)
require.NoError(t, err)
require.Equal(t, afdoRevBase, lastRollRev.Id)
require.Equal(t, afdoRevNext, tipRev.Id)
require.Equal(t, 1, len(notRolledRevs))
require.Equal(t, afdoRevNext, notRolledRevs[0].Id)
// Upload a CL.
// Mock the initial change creation.
commitMsg := `Roll AFDO from 66.0.3336.0_rc-r1-merged to 66.0.3337.0_rc-r1-merged
This CL may cause a small binary size increase, roughly proportional
to how long it's been since our last AFDO profile roll. For larger
increases (around or exceeding 100KB), please file a bug against
gbiv@chromium.org. Additional context: https://crbug.com/805539
Please note that, despite rolling to chrome/android, this profile is
used for both Linux and Android.
If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
fake.server.com
Please CC reviewer@chromium.org on the revert to ensure that a human
is aware of the problem.
To report a problem with the AutoRoller itself, please file a bug:
https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug
Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+/master/autoroll/README.md
Tbr: reviewer@chromium.org
`
subject := strings.Split(commitMsg, "\n")[0]
reqBody := []byte(fmt.Sprintf(`{"project":"%s","subject":"%s","branch":"%s","topic":"","status":"NEW","base_commit":"%s"}`, rm.noCheckoutRepoManager.gerritConfig.Project, subject, rm.parentBranch, parentMaster))
ci := gerrit.ChangeInfo{
ChangeId: "123",
Id: "123",
Issue: 123,
Revisions: map[string]*gerrit.Revision{
"ps1": {
ID: "ps1",
Number: 1,
},
},
}
respBody, err := json.Marshal(ci)
require.NoError(t, err)
respBody = append([]byte(")]}'\n"), respBody...)
urlmock.MockOnce("https://fake-skia-review.googlesource.com/a/changes/", mockhttpclient.MockPostDialogueWithResponseCode("application/json", reqBody, respBody, 201))
// Mock the edit of the change to update the commit message.
reqBody = []byte(fmt.Sprintf(`{"message":"%s"}`, strings.Replace(commitMsg, "\n", "\\n", -1)))
urlmock.MockOnce("https://fake-skia-review.googlesource.com/a/changes/123/edit:message", mockhttpclient.MockPutDialogue("application/json", reqBody, []byte("")))
// Mock the request to modify the version file.
reqBody = []byte(tipRev.Id)
url := fmt.Sprintf("https://fake-skia-review.googlesource.com/a/changes/123/edit/%s", url.QueryEscape(AFDO_VERSION_FILE_PATH))
urlmock.MockOnce(url, mockhttpclient.MockPutDialogue("", reqBody, []byte("")))
// Mock the request to publish the change edit.
reqBody = []byte(`{"notify":"ALL"}`)
urlmock.MockOnce("https://fake-skia-review.googlesource.com/a/changes/123/edit:publish", mockhttpclient.MockPostDialogue("application/json", reqBody, []byte("")))
// Mock the request to load the updated change.
respBody, err = json.Marshal(ci)
require.NoError(t, err)
respBody = append([]byte(")]}'\n"), respBody...)
urlmock.MockOnce("https://fake-skia-review.googlesource.com/a/changes/123/detail?o=ALL_REVISIONS", mockhttpclient.MockGetDialogue(respBody))
// Mock the request to set the CQ.
reqBody = []byte(`{"labels":{"Code-Review":1,"Commit-Queue":2},"message":"","reviewers":[{"reviewer":"reviewer@chromium.org"}]}`)
urlmock.MockOnce("https://fake-skia-review.googlesource.com/a/changes/123/revisions/ps1/review", mockhttpclient.MockPostDialogue("application/json", reqBody, []byte("")))
issue, err := rm.CreateNewRoll(ctx, lastRollRev, tipRev, notRolledRevs, emails, cqExtraTrybots, false)
require.NoError(t, err)
require.Equal(t, ci.Issue, issue)
}
func TestChromiumAFDOConfigValidation(t *testing.T) {
unittest.SmallTest(t)
cfg := afdoCfg()
// Fill in some fields which are not supplied above.
cfg.ParentRepo = "dummy"
cfg.CommitMsgTmpl = TMPL_COMMIT_MSG_AFDO
cfg.GCSBucket = AFDO_GS_BUCKET
cfg.GCSPath = AFDO_GS_PATH
cfg.VersionFile = AFDO_VERSION_FILE_PATH
cfg.VersionRegex = AFDO_VERSION_REGEX
require.NoError(t, cfg.Validate())
// The only fields come from the nested Configs, so exclude them and
// verify that we fail validation.
cfg = &SemVerGCSRepoManagerConfig{}
require.Error(t, cfg.Validate())
}
func TestAFDORepoManagerCurrentRevNotFound(t *testing.T) {
unittest.LargeTest(t)
ctx, rm, urlmock, mockParent, parent, cleanup := setupAfdo(t)
defer cleanup()
// Sanity check.
mockGSObject(t, urlmock, AFDO_GS_BUCKET, AFDO_GS_PATH, afdoRevPrev, afdoTimePrev)
prev, err := rm.GetRevision(ctx, afdoRevPrev)
require.NoError(t, err)
require.Equal(t, afdoRevPrev, prev.Id)
mockGSObject(t, urlmock, AFDO_GS_BUCKET, AFDO_GS_PATH, afdoRevBase, afdoTimeBase)
base, err := rm.GetRevision(ctx, afdoRevBase)
require.NoError(t, err)
require.Equal(t, afdoRevBase, base.Id)
mockGSObject(t, urlmock, AFDO_GS_BUCKET, AFDO_GS_PATH, afdoRevNext, afdoTimeNext)
next, err := rm.GetRevision(ctx, afdoRevNext)
require.NoError(t, err)
require.Equal(t, afdoRevNext, next.Id)
// Roll to a revision which is not in the GCS bucket.
parent.Add(context.Background(), AFDO_VERSION_FILE_PATH, "BOGUS REV")
parent.Commit(context.Background())
mockParent.MockGetCommit(ctx, "master")
parentMaster, err := git.GitDir(parent.Dir()).RevParse(ctx, "HEAD")
require.NoError(t, err)
mockParent.MockReadFile(ctx, AFDO_VERSION_FILE_PATH, parentMaster)
mockGSList(t, urlmock, AFDO_GS_BUCKET, AFDO_GS_PATH, map[string]string{
afdoRevBase: afdoTimeBase,
afdoRevPrev: afdoTimePrev,
afdoRevNext: afdoTimeNext,
})
lastRollRev, tipRev, notRolledRevs, err := rm.Update(ctx)
require.NoError(t, err)
// We ignore the bogus rev and just pretend that the last rolled rev is
// the second-most-recent revision.
require.Equal(t, afdoRevBase, lastRollRev.Id)
require.Equal(t, afdoRevNext, tipRev.Id)
require.Equal(t, 1, len(notRolledRevs))
require.Equal(t, afdoRevNext, notRolledRevs[0].Id)
}