blob: 59a47ced5108e80ebb45bf14999f610ff95c7930 [file] [log] [blame]
// Copyright 2019 The Chromium OS 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 test defines a branch_util-specific test harness.
package test
import (
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
mv "infra/cros/internal/chromeosversion"
"infra/cros/internal/git"
"infra/cros/internal/manifestutil"
"infra/cros/internal/repo"
rh "infra/cros/internal/repoharness"
"go.chromium.org/luci/common/errors"
)
// This is intended to be a more specific version of RepoHarness
// that caters to the specific setup of the ChromeOS project.
const (
// RemoteCros is the remote name for the external Chromiumos project.
RemoteCros = "cros"
// RemoteCrosInternal is the remote name for the internal Chromiumos project.
RemoteCrosInternal = "cros-internal"
// ProjectManifest is the name of the external manifest repository.
ProjectManifest = "manifest"
// ProjectManifestInternal is the name of the internal manifest repository.
ProjectManifestInternal = "manifest-internal"
)
var (
// DefaultRemotes is a list of the default remotes for a Chromiumos checkout.
DefaultRemotes = []repo.Remote{
{Name: RemoteCros},
{Name: RemoteCrosInternal},
}
// DefaultVersionProject is the default project that contains version information.
DefaultVersionProject = repo.Project{
Name: "chromiumos/overlays/chromiumos-overlay",
Path: "src/third_party/chromiumos-overlay",
RemoteName: RemoteCros,
}
// DefaultManifestProject is the default external manifest project.
DefaultManifestProject = repo.Project{
Name: "chromiumos/" + ProjectManifest,
Path: ProjectManifest,
RemoteName: RemoteCros,
}
// DefaultManifestInternalProject is the default internal manifest project.
DefaultManifestInternalProject = repo.Project{
Name: "chromiumos/" + ProjectManifestInternal,
Path: ProjectManifestInternal,
RemoteName: RemoteCrosInternal,
}
// DefaultProjects contains a list of projects that should be included in
// tests by default.
DefaultProjects = []repo.Project{
// Version file project.
DefaultVersionProject,
// Manifest projects.
DefaultManifestProject,
DefaultManifestInternalProject,
}
// DefaultCrosHarnessConfig is the default config for a CrOS repo harness.
DefaultCrosHarnessConfig = CrosRepoHarnessConfig{
Manifest: repo.Manifest{
Projects: DefaultProjects,
Remotes: DefaultRemotes,
Default: repo.Default{
RemoteName: RemoteCros,
Revision: "refs/heads/main",
},
},
VersionProject: DefaultVersionProject.Name,
}
)
// CrosRepoHarness is a cros-specific test harness.
type CrosRepoHarness struct {
Harness rh.RepoHarness
// Version info project information.
versionProject *repo.Project
}
// CrosRepoHarnessConfig contains config for a CrosRepoHarness.
type CrosRepoHarnessConfig struct {
// Initialize() will create a test harness with
// the appropriate remote repos and a local repo.
// Both remote and local repos will have the appropriate
// projects created (with initialized git repos inside them).
Manifest repo.Manifest
// Version info project name. Should exist in Manifest.
VersionProject string
}
const (
attrRegexpTemplate = "%s=\"[^\"]*\""
)
// Initialize creates a new CrosRepoHarnes based on config.
func (r *CrosRepoHarness) Initialize(config *CrosRepoHarnessConfig) error {
if config.VersionProject == "" {
return fmt.Errorf("version project not specified")
}
// If VersionProject is set, check that it is in the manifest.
foundVersionProject := false
for i := range config.Manifest.Projects {
if config.VersionProject == config.Manifest.Projects[i].Name {
r.versionProject = &config.Manifest.Projects[i]
foundVersionProject = true
break
}
}
if !foundVersionProject {
return fmt.Errorf("version project %v does not exist in specified manifest", config.VersionProject)
}
err := r.Harness.Initialize(&rh.Config{
Manifest: config.Manifest,
})
if err != nil {
return err
}
return nil
}
// Teardown tears down a CrosRepoHarness.
func (r *CrosRepoHarness) Teardown() error {
return r.Harness.Teardown()
}
func (r *CrosRepoHarness) assertInitialized() error {
if r.Harness.HarnessRoot() == "" {
return fmt.Errorf("harness needs to be initialized")
}
return nil
}
func projectRef(project repo.Project) string {
if project.Upstream != "" {
return git.StripRefs(project.Upstream)
}
return git.StripRefs(project.Revision)
}
func multicheckoutBranchName(project *repo.Project, branch, sourceBranch string) string {
pid := projectRef(*project)
// If branch was the result of a rename, then the project ref will start with
// sourceBranch-. This function removes that bit.
if sourceBranch != "" {
pid = strings.Replace(pid, sourceBranch+"-", "", 1)
}
branchName := fmt.Sprintf("%s-%s", branch, pid)
return branchName
}
// versionFileContents returns the contents of a basic ChromeOS version file.
func versionFileContents(version mv.VersionInfo) string {
contents := fmt.Sprintf("#!/bin/sh\n"+
"CHROME_BRANCH=%d\nCHROMEOS_BUILD=%d\nCHROMEOS_BRANCH=%d\n,CHROMEOS_PATCH=%d\n",
version.ChromeBranch, version.BuildNumber, version.BranchBuildNumber, version.PatchNumber)
return contents
}
// SetVersion sets the version file contents for the specified branch.
// If branch is not set, will use the version project's revision.
func (r *CrosRepoHarness) SetVersion(branch string, version mv.VersionInfo) error {
if err := r.assertInitialized(); err != nil {
return err
}
if version.VersionFile == "" {
version.VersionFile = mv.VersionFileProjectPath
}
versionFile := rh.File{
Name: version.VersionFile,
Contents: []byte(versionFileContents(version)),
}
if branch == "" {
branch = git.StripRefs(r.versionProject.Revision)
}
_, err := r.Harness.AddFile(rh.GetRemoteProject(*r.versionProject), branch, versionFile)
if err != nil {
return errors.Annotate(err, "failed to add version file").Err()
}
return nil
}
// AssertCrosBranches asserts that remote projects have the expected chromiumos branches.
func (r *CrosRepoHarness) AssertCrosBranches(branches []string) error {
manifest := r.Harness.Manifest()
singleProjects := manifest.GetSingleCheckoutProjects()
for _, project := range singleProjects {
if err := r.Harness.AssertProjectBranches(rh.GetRemoteProject(*project), append(branches, "main")); err != nil {
return err
}
}
multiProjects := manifest.GetMultiCheckoutProjects()
for _, project := range multiProjects {
projectBranches := []string{"main"}
for _, branch := range branches {
projectBranches = append(projectBranches, multicheckoutBranchName(project, branch, ""))
}
if err := r.Harness.AssertProjectBranches(rh.GetRemoteProject(*project), projectBranches); err != nil {
return err
}
}
pinnedProjects := manifest.GetPinnedProjects()
for _, project := range pinnedProjects {
if err := r.Harness.AssertProjectBranches(
rh.GetRemoteProject(*project), []string{"main", projectRef(*project)}); err != nil {
return err
}
}
totProjects := manifest.GetTotProjects()
for _, project := range totProjects {
if err := r.Harness.AssertProjectBranches(rh.GetRemoteProject(*project), []string{"main"}); err != nil {
return err
}
}
return nil
}
// AssertCrosBranchesMissing asserts that the specified chromium branch does not exist
// in any projects.
func (r *CrosRepoHarness) AssertCrosBranchesMissing(branches []string) error {
branchAssertFn := r.Harness.AssertProjectBranchesMissing
manifest := r.Harness.Manifest()
singleProjects := manifest.GetSingleCheckoutProjects()
for _, project := range singleProjects {
if err := branchAssertFn(rh.GetRemoteProject(*project), append(branches, "main")); err != nil {
return err
}
}
multiProjects := manifest.GetMultiCheckoutProjects()
for _, project := range multiProjects {
projectBranches := []string{"main"}
for _, branch := range branches {
projectBranches = append(projectBranches, multicheckoutBranchName(project, branch, ""))
}
if err := branchAssertFn(rh.GetRemoteProject(*project), projectBranches); err != nil {
return err
}
}
// Don't care about pinned/ToT -- nothing would have been created for a particular branch.
return nil
}
func (r *CrosRepoHarness) getRecentProjectSnapshot(project rh.RemoteProject) (string, error) {
remoteSnapshot, err := r.Harness.GetRecentRemoteSnapshot(project.RemoteName)
if err != nil {
return "", err
}
return filepath.Join(remoteSnapshot, project.ProjectName), nil
}
// AssertCrosBranchFromManifest asserts that the specified CrOS branch descends
// from the given manifest.
// sourceBranch should be set if branch was the result of a branch rename.
func (r *CrosRepoHarness) AssertCrosBranchFromManifest(manifest repo.Manifest, branch string, sourceBranch string) error {
projectSnapshots := make(map[string]string)
var err error
for _, project := range manifest.Projects {
if projectSnapshots[project.Name], err = r.getRecentProjectSnapshot(rh.GetRemoteProject(project)); err != nil {
return err
}
}
// For non-pinned/tot projects, check that each project has the revision specified in the manifest
// as an ancestor.
singleProjects := manifest.GetSingleCheckoutProjects()
for _, project := range singleProjects {
projectSnapshot := projectSnapshots[project.Name]
err := r.Harness.AssertProjectBranchHasAncestor(
rh.GetRemoteProject(*project),
branch,
projectSnapshot,
project.Revision)
if err != nil {
return err
}
}
multiProjects := manifest.GetMultiCheckoutProjects()
for _, project := range multiProjects {
projectSnapshot := projectSnapshots[project.Name]
err := r.Harness.AssertProjectBranchHasAncestor(
rh.GetRemoteProject(*project),
multicheckoutBranchName(project, branch, sourceBranch),
projectSnapshot, project.Revision)
if err != nil {
return err
}
}
// For pinned/tot projects, check that each project is unchanged.
pinnedProjects := manifest.GetPinnedProjects()
for _, project := range pinnedProjects {
projectSnapshot := projectSnapshots[project.Name]
errs := []error{
r.Harness.AssertProjectBranchEqual(rh.GetRemoteProject(*project), "main", projectSnapshot),
r.Harness.AssertProjectBranchEqual(rh.GetRemoteProject(*project), projectRef(*project), projectSnapshot),
}
for _, err = range errs {
if err != nil {
return err
}
}
}
totProjects := manifest.GetTotProjects()
for _, project := range totProjects {
projectSnapshot := projectSnapshots[project.Name]
if err = r.Harness.AssertProjectBranchEqual(rh.GetRemoteProject(*project), "main", projectSnapshot); err != nil {
return err
}
}
return nil
}
// AssertCrosVersion asserts that chromeos_version.sh has the expected version numbers.
func (r *CrosRepoHarness) AssertCrosVersion(branch string, version mv.VersionInfo) error {
if r.versionProject == nil {
return fmt.Errorf("VersionProject was not set in config")
}
if version.VersionFile == "" {
log.Printf("null version file, using default %s", mv.VersionFileProjectPath)
version.VersionFile = mv.VersionFileProjectPath
}
manifest := r.Harness.Manifest()
project, err := manifest.GetProjectByName(r.versionProject.Name)
if err != nil {
return errors.Annotate(err, "error getting chromeos version project %s", project.Name).Err()
}
versionFileContents, err := r.Harness.ReadFile(rh.GetRemoteProject(*project), branch, version.VersionFile)
if err != nil {
return errors.Annotate(err, "could not read version file %s", version.VersionFile).Err()
}
versionInfo, err := mv.ParseVersionInfo(versionFileContents)
if err != nil {
return errors.Annotate(err, "could not parse version file %s", version.VersionFile).Err()
}
if !mv.VersionsEqual(versionInfo, version) {
versionInfo.VersionFile = ""
version.VersionFile = ""
return fmt.Errorf("version mismatch. expected: %v actual %v", version, versionInfo)
}
return nil
}
// AssertNoDefaultRevisions asserts that the given manifest has no default revisions.
func AssertNoDefaultRevisions(manifest repo.Manifest) error {
if manifest.Default.Revision != "" {
return fmt.Errorf("manifest <default> has revision %s", manifest.Default.Revision)
}
for _, remote := range manifest.Remotes {
if remote.Revision != "" {
return fmt.Errorf("<remote> %s has revision %s", remote.Name, remote.Revision)
}
}
return nil
}
func assertEqual(expected, actual string) error {
if expected != actual {
return fmt.Errorf("expected: %s got %s", expected, actual)
}
return nil
}
func projectInList(project repo.Project, projects []*repo.Project) bool {
for _, p := range projects {
if project.Path == p.Path {
return true
}
}
return false
}
// AssertProjectRevisionsMatchBranch asserts that the project revisions match the given CrOS branch.
func (r *CrosRepoHarness) AssertProjectRevisionsMatchBranch(manifest repo.Manifest, branch, sourceBranch string) error {
originalManifest := r.Harness.Manifest()
singleProjects := originalManifest.GetSingleCheckoutProjects()
multiProjects := originalManifest.GetMultiCheckoutProjects()
pinnedProjects := originalManifest.GetPinnedProjects()
totProjects := originalManifest.GetTotProjects()
for _, project := range manifest.Projects {
if projectInList(project, singleProjects) {
if err := assertEqual(git.NormalizeRef(branch), project.Revision); err != nil {
return errors.Annotate(err, "mismatch for project %s", project.Path).Err()
}
}
if projectInList(project, multiProjects) {
originalManifest := r.Harness.Manifest()
originalProject, err := originalManifest.GetProjectByPath(project.Path)
if err != nil {
return errors.Annotate(err, "could not get project %s from harness manifest", project.Path).Err()
}
expected := git.NormalizeRef(multicheckoutBranchName(originalProject, branch, sourceBranch))
if err := assertEqual(expected, project.Revision); err != nil {
return errors.Annotate(err, "mismatch for project %s", project.Path).Err()
}
}
if projectInList(project, pinnedProjects) {
// Get original revision of project. Make sure that it and the current revision (which will be a SHA)
// are the same ref.
originalProject, err := originalManifest.GetProjectByPath(project.Path)
if err != nil {
return errors.Annotate(err, "could not get project %s from harness manifest", project.Path).Err()
}
pinnedBranch := git.StripRefs(originalProject.Revision)
projectPath := r.Harness.GetRemotePath(rh.GetRemoteProject(project))
expected, err := git.GetGitRepoRevision(projectPath, pinnedBranch)
if err != nil {
return errors.Annotate(err, "failed to fetch git revision for %s:%s", project.Path, pinnedBranch).Err()
}
if err := assertEqual(expected, project.Revision); err != nil {
return errors.Annotate(err, "mismatch for project %s", project.Path).Err()
}
}
if projectInList(project, totProjects) {
// With the COIL initiative underway testing applications use "main" and production code uses "master"
// Some unit tests will call production code then verify the results. In some instances it will insert
// "master" where we would normall expect "main". This is currently a patch to a problem that will be
// fixed when the COIL initiative is fully completed.
// TODO: Check only for "refs/heads/main" once COIL is completed
if project.Revision != "refs/heads/master" && project.Revision != "refs/heads/main" {
return fmt.Errorf("mismatch for project %s. Expected %s or %s, got %s", project.Path, "refs/heads/master", "refs/heads/main", project.Revision)
}
}
}
return nil
}
func getLocalCheckout(r *CrosRepoHarness, project rh.RemoteProject, branch string) (string, error) {
// Create local checkout of project at branch.
tmpDir, err := ioutil.TempDir(r.Harness.HarnessRoot(), "tmp-repo")
if err != nil {
return "", err
}
remotePath := r.Harness.GetRemotePath(project)
errs := []error{
git.Clone(remotePath, tmpDir),
git.Checkout(tmpDir, branch),
}
for _, err := range errs {
if err != nil {
return "", errors.Annotate(err, "failed to checkout branch %s in project %s", branch, project.ProjectName).Err()
}
}
return tmpDir, nil
}
func cleanupLocalCheckout(checkout string) {
os.RemoveAll(checkout)
}
// Use these variables to call the functions so that they can be mocked for testing purposes.
var getLocalCheckoutFunc = getLocalCheckout
var cleanupLocalCheckoutFunc = cleanupLocalCheckout
// AssertManifestProjectRepaired asserts that the specified manifest XML files in the specified branch
// of a project were repaired.
// This function assumes that r.Harness.SyncLocalCheckout() has just been run.
func (r *CrosRepoHarness) AssertManifestProjectRepaired(
project rh.RemoteProject, branch string, manifestFiles []string) error {
tmpDir, err := getLocalCheckoutFunc(r, project, branch)
defer cleanupLocalCheckoutFunc(tmpDir)
if err != nil {
return err
}
for _, file := range manifestFiles {
filePath := filepath.Join(tmpDir, file)
manifest, err := manifestutil.LoadManifestFromFile(filePath)
if err != nil {
return errors.Annotate(err, "failed to load manifest file %s", file).Err()
}
// Have to resolve implicit links to set the appropriate remote for each
// project so that the asserts work.
manifest.ResolveImplicitLinks()
if err = AssertNoDefaultRevisions(*manifest); err != nil {
return errors.Annotate(err, "manifest %s has error", file).Err()
}
if err = r.AssertProjectRevisionsMatchBranch(*manifest, branch, ""); err != nil {
return errors.Annotate(err, "manifest %s has error", file).Err()
}
}
return nil
}
func getComments(file string) []string {
commentRegex := regexp.MustCompile("<!--.*-->")
return commentRegex.FindAllString(file, -1)
}
// AssertCommentsPersist asserts that the comments from the source manifests (i.e. the manifests of the source branch)
// persist in the new branch.
func (r *CrosRepoHarness) AssertCommentsPersist(
project rh.RemoteProject, branch string, sourceManifestFiles map[string]string) error {
// Create local checkout of project at branch.
tmpDir, err := getLocalCheckoutFunc(r, project, branch)
defer cleanupLocalCheckoutFunc(tmpDir)
if err != nil {
return err
}
for file, expectedContents := range sourceManifestFiles {
// Manifest filenames are the same in source and destination branches, so we simply change the path to get the
// new manifest.
filepath := filepath.Join(tmpDir, file)
contents, err := ioutil.ReadFile(filepath)
if err != nil {
return errors.Annotate(err, "failed to load manifest file %s", file).Err()
}
expectedComments := getComments(expectedContents)
comments := getComments(string(contents))
if !reflect.DeepEqual(expectedComments, comments) {
return fmt.Errorf("Comment mismatch. Expected %v got %v", expectedComments, comments)
}
}
return nil
}
func removeAttrs(manifest []byte) []byte {
fetchRegexp := regexp.MustCompile(fmt.Sprintf(attrRegexpTemplate, "fetch"))
revisionRegexp := regexp.MustCompile(fmt.Sprintf(attrRegexpTemplate, "revision"))
upstreamRegexp := regexp.MustCompile(fmt.Sprintf(attrRegexpTemplate, "upstream"))
manifest = fetchRegexp.ReplaceAll(manifest, []byte{})
manifest = revisionRegexp.ReplaceAll(manifest, []byte{})
return upstreamRegexp.ReplaceAll(manifest, []byte{})
}
func stripManifest(manifest []byte) []byte {
return regexp.MustCompile(`\s*`).ReplaceAll(manifest, []byte{})
}
// AssertMinimalManifestChanges asserts that the manifests in project/branch exactly match the expected contents
// with three exceptions:
// 1. Ignore whitespace.
// 2. Ignore revision attributes (which are checked in other tests).
// 3. Ignore fetch attributes (which are just mock values in the test harness).
// 4. Ignore upstream attributes (doing so makes branch_util_test.go cleaner and they aren't really relevant anyways)
func (r *CrosRepoHarness) AssertMinimalManifestChanges(
project rh.RemoteProject, branch string, expectedManifestFiles map[string]string) error {
// Create local checkout of project at branch.
tmpDir, err := getLocalCheckoutFunc(r, project, branch)
defer cleanupLocalCheckoutFunc(tmpDir)
if err != nil {
return err
}
// Check that manifests in project/branch exactly match contents of expectedManifestFiles
for file, expectedContents := range expectedManifestFiles {
filepath := filepath.Join(tmpDir, file)
contents, err := ioutil.ReadFile(filepath)
if err != nil {
return errors.Annotate(err, "failed to load manifest file %s", file).Err()
}
expectedContents = string(removeAttrs([]byte(expectedContents)))
contents = removeAttrs(contents)
if !reflect.DeepEqual(stripManifest([]byte(expectedContents)), stripManifest(contents)) {
return fmt.Errorf("Manifest mismatch for %s. Expected\n%v\ngot\n%v", file, string(expectedContents), string(contents))
}
}
return nil
}