blob: ae5fe8fd335c2ae046612a0475d65e36e538721a [file] [log] [blame]
package repo_manager
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
"testing"
"github.com/stretchr/testify/require"
"go.skia.org/infra/autoroll/go/codereview"
"go.skia.org/infra/autoroll/go/config"
"go.skia.org/infra/autoroll/go/repo_manager/child"
"go.skia.org/infra/autoroll/go/repo_manager/parent"
"go.skia.org/infra/autoroll/go/revision"
"go.skia.org/infra/go/deepequal/assertdeep"
"go.skia.org/infra/go/gerrit"
gerrit_mocks "go.skia.org/infra/go/gerrit/mocks"
"go.skia.org/infra/go/git"
gitiles_mocks "go.skia.org/infra/go/gitiles/mocks"
"go.skia.org/infra/go/mockhttpclient"
"go.skia.org/infra/go/util"
)
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"
afdoGsBucket = "chromeos-prebuilt"
afdoGsPath = "afdo-job/llvm"
// Example name: chromeos-chrome-amd64-63.0.3239.57_rc-r1.afdo.bz2
afdoVersionRegex = ("^chromeos-chrome-amd64-" + // Prefix
"(\\d+)\\.(\\d+)\\.(\\d+)\\.0" + // Version
"_rc-r(\\d+)" + // Revision
"-merged\\.afdo\\.bz2$") // Suffix
afdoShortRevRegex = "(\\d+)\\.(\\d+)\\.(\\d+)\\.0_rc-r(\\d+)-merged"
afdoVersionFilePath = "chrome/android/profiles/newest.txt"
)
func afdoCfg() *config.ParentChildRepoManagerConfig {
return &config.ParentChildRepoManagerConfig{
Parent: &config.ParentChildRepoManagerConfig_GitilesParent{
GitilesParent: &config.GitilesParentConfig{
Gitiles: &config.GitilesConfig{
Branch: git.MainBranch,
RepoUrl: "todo.git",
},
Dep: &config.DependencyConfig{
Primary: &config.VersionFileConfig{
Id: "AFDO",
File: []*config.VersionFileConfig_File{
{Path: afdoVersionFilePath},
},
},
},
Gerrit: &config.GerritConfig{
Url: "https://fake-skia-review.googlesource.com",
Project: "fake-gerrit-project",
Config: config.GerritConfig_CHROMIUM,
},
},
},
Child: &config.ParentChildRepoManagerConfig_SemverGcsChild{
SemverGcsChild: &config.SemVerGCSChildConfig{
Gcs: &config.GCSChildConfig{
GcsBucket: afdoGsBucket,
GcsPath: afdoGsPath,
},
ShortRevRegex: afdoShortRevRegex,
VersionRegex: afdoVersionRegex,
},
},
}
}
func gerritCR(t *testing.T, g gerrit.GerritInterface, client *http.Client) codereview.CodeReview {
rv, err := codereview.NewGerrit(&config.GerritConfig{
Url: "https://skia-review.googlesource.com",
Project: "skia",
Config: config.GerritConfig_CHROMIUM,
}, g, client)
require.NoError(t, err)
return rv
}
func setupAfdo(t *testing.T) (*parentChildRepoManager, *gitiles_mocks.GitilesRepo, *gerrit_mocks.GerritInterface, *mockhttpclient.URLMock) {
reg := setupRegistry(t)
cfg := afdoCfg()
parentCfg := cfg.GetGitilesParent()
p, parentGitiles, parentGerrit := parent.NewGitilesFileForTesting(t, parentCfg, reg)
urlmock := mockhttpclient.NewURLMock()
c, err := child.NewSemVerGCS(t.Context(), cfg.GetSemverGcsChild(), reg, urlmock.Client())
require.NoError(t, err)
// Create the RepoManager.
rm := &parentChildRepoManager{
Parent: p,
Child: c,
}
// Mock requests for Update().
fileContents := map[string]string{
afdoVersionFilePath: afdoRevBase,
}
parent.MockGitilesFileForUpdate(parentGitiles, cfg.GetGitilesParent(), noCheckoutParentHead, fileContents)
mockGSList(t, urlmock, afdoGsBucket, afdoGsPath, map[string]string{
afdoRevBase: afdoTimeBase,
})
// Mock the "list" call twice, since Update uses both LogRevisions and getAllRevisions.
mockGSList(t, urlmock, afdoGsBucket, afdoGsPath, map[string]string{
afdoRevBase: afdoTimeBase,
})
mockGSObject(t, urlmock, afdoGsBucket, afdoGsPath, afdoRevBase, afdoTimeBase)
// Initial update. Everything up to date.
_, _, _ = updateAndAssert(t, rm, parentGitiles, parentGerrit, urlmock)
return rm, parentGitiles, parentGerrit, urlmock
}
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://storage.googleapis.com/storage/v1/b/%s/o?alt=json&delimiter=&endOffset=&includeFoldersAsPrefixes=false&includeTrailingDelimiter=false&matchGlob=&pageToken=&prefix=%s&prettyPrint=false&projection=full&startOffset=&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://storage.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) {
rm, parentGitiles, parentGerrit, urlmock := setupAfdo(t)
cfg := afdoCfg()
// Mock requests for Update.
oldContent := map[string]string{
afdoVersionFilePath: afdoRevBase,
}
parent.MockGitilesFileForUpdate(parentGitiles, cfg.GetGitilesParent(), noCheckoutParentHead, oldContent)
mockGSList(t, urlmock, afdoGsBucket, afdoGsPath, map[string]string{
afdoRevBase: afdoTimeBase,
})
// Mock the "list" call twice, since Update uses both LogRevisions and getAllRevisions.
mockGSList(t, urlmock, afdoGsBucket, afdoGsPath, map[string]string{
afdoRevBase: afdoTimeBase,
})
mockGSObject(t, urlmock, afdoGsBucket, afdoGsPath, afdoRevBase, afdoTimeBase)
// Update.
lastRollRev, tipRev, notRolledRevs := updateAndAssert(t, rm, parentGitiles, parentGerrit, urlmock)
require.Equal(t, afdoRevBase, lastRollRev.Id)
require.Equal(t, afdoRevBase, tipRev.Id)
mockGSObject(t, urlmock, afdoGsBucket, afdoGsPath, afdoRevPrev, afdoTimePrev)
prev, err := rm.GetRevision(t.Context(), afdoRevPrev)
require.NoError(t, err)
require.Equal(t, afdoRevPrev, prev.Id)
mockGSObject(t, urlmock, afdoGsBucket, afdoGsPath, afdoRevBase, afdoTimeBase)
base, err := rm.GetRevision(t.Context(), afdoRevBase)
require.NoError(t, err)
require.Equal(t, afdoRevBase, base.Id)
mockGSObject(t, urlmock, afdoGsBucket, afdoGsPath, afdoRevNext, afdoTimeNext)
next, err := rm.GetRevision(t.Context(), afdoRevNext)
require.NoError(t, err)
require.Equal(t, afdoRevNext, next.Id)
require.Equal(t, 0, len(notRolledRevs))
// There's a new version.
parent.MockGitilesFileForUpdate(parentGitiles, cfg.GetGitilesParent(), noCheckoutParentHead, oldContent)
mockGSList(t, urlmock, afdoGsBucket, afdoGsPath, map[string]string{
afdoRevBase: afdoTimeBase,
afdoRevNext: afdoTimeNext,
})
// Mock the "list" call twice, since Update uses both LogRevisions and getAllRevisions.
mockGSList(t, urlmock, afdoGsBucket, afdoGsPath, map[string]string{
afdoRevBase: afdoTimeBase,
afdoRevNext: afdoTimeNext,
})
mockGSObject(t, urlmock, afdoGsBucket, afdoGsPath, afdoRevBase, afdoTimeBase)
lastRollRev, tipRev, notRolledRevs = updateAndAssert(t, rm, parentGitiles, parentGerrit, urlmock)
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.
newContent := map[string]string{
afdoVersionFilePath: afdoRevNext + "\n",
}
parent.MockGitilesFileForCreateNewRoll(parentGitiles, parentGerrit, cfg.GetGitilesParent(), noCheckoutParentHead, fakeCommitMsgMock, oldContent, newContent, fakeReviewers)
_, err = rm.CreateNewRoll(t.Context(), lastRollRev, tipRev, notRolledRevs, fakeReviewers, false, false, fakeCommitMsg)
require.NoError(t, err)
assertExpectations(t, parentGitiles, parentGerrit, urlmock)
}
func TestAFDORepoManagerCurrentRevNotFound(t *testing.T) {
rm, parentGitiles, parentGerrit, urlmock := setupAfdo(t)
cfg := afdoCfg()
// Sanity check.
mockGSObject(t, urlmock, afdoGsBucket, afdoGsPath, afdoRevPrev, afdoTimePrev)
prev, err := rm.GetRevision(t.Context(), afdoRevPrev)
require.NoError(t, err)
require.Equal(t, afdoRevPrev, prev.Id)
mockGSObject(t, urlmock, afdoGsBucket, afdoGsPath, afdoRevBase, afdoTimeBase)
base, err := rm.GetRevision(t.Context(), afdoRevBase)
require.NoError(t, err)
require.Equal(t, afdoRevBase, base.Id)
mockGSObject(t, urlmock, afdoGsBucket, afdoGsPath, afdoRevNext, afdoTimeNext)
next, err := rm.GetRevision(t.Context(), afdoRevNext)
require.NoError(t, err)
require.Equal(t, afdoRevNext, next.Id)
// We've rolled to a revision which is not in the GCS bucket.
fileContents := map[string]string{
afdoVersionFilePath: "BOGUS_REV",
}
parent.MockGitilesFileForUpdate(parentGitiles, cfg.GetGitilesParent(), noCheckoutParentHead, fileContents)
mockGSList(t, urlmock, afdoGsBucket, afdoGsPath, map[string]string{
afdoRevBase: afdoTimeBase,
afdoRevPrev: afdoTimePrev,
afdoRevNext: afdoTimeNext,
})
mockGSObject(t, urlmock, afdoGsBucket, afdoGsPath, "BOGUS_REV", afdoTimePrev)
lastRollRev, tipRev, notRolledRevs := updateAndAssert(t, rm, parentGitiles, parentGerrit, urlmock)
expect := &revision.Revision{
Id: "BOGUS_REV",
Checksum: "76c69f92576495db1a",
Display: "BOGUS_REV",
URL: "https://storage.googleapis.com/storage/v1/b/chromeos-prebuilt/o/afdo-job%2Fllvm%2FBOGUS_REV?alt=json&prettyPrint=false&projection=full",
}
expect.Timestamp = lastRollRev.Timestamp
assertdeep.Equal(t, expect, lastRollRev)
require.False(t, util.TimeIsZero(lastRollRev.Timestamp))
require.Equal(t, afdoRevNext, tipRev.Id)
require.Equal(t, 1, len(notRolledRevs))
require.Equal(t, afdoRevNext, notRolledRevs[0].Id)
require.True(t, urlmock.Empty())
// Now try again, but don't mock the bogus rev in GCS. We should still
// come up with the same lastRollRev.Id, but the Revision will otherwise
// be empty.
parent.MockGitilesFileForUpdate(parentGitiles, cfg.GetGitilesParent(), noCheckoutParentHead, fileContents)
mockGSList(t, urlmock, afdoGsBucket, afdoGsPath, map[string]string{
afdoRevBase: afdoTimeBase,
afdoRevPrev: afdoTimePrev,
afdoRevNext: afdoTimeNext,
})
// Mock the "list" call twice, since GetRevision falls back to scanning the
// GCS directory after the initial GetFileObjectAttrs call fails.
mockGSList(t, urlmock, afdoGsBucket, afdoGsPath, map[string]string{
afdoRevBase: afdoTimeBase,
afdoRevPrev: afdoTimePrev,
afdoRevNext: afdoTimeNext,
})
lastRollRev, tipRev, notRolledRevs = updateAndAssert(t, rm, parentGitiles, parentGerrit, urlmock)
assertdeep.Equal(t, &revision.Revision{
Id: "BOGUS_REV",
InvalidReason: "Failed to retrieve revision.",
}, lastRollRev)
require.Equal(t, afdoRevNext, tipRev.Id)
require.Equal(t, 1, len(notRolledRevs))
require.Equal(t, afdoRevNext, notRolledRevs[0].Id)
require.True(t, urlmock.Empty())
}