blob: fb12f3e69b5aace8e0b8cf33d8191861764bfefa [file] [log] [blame]
// Copyright 2015 The Vanadium 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 project_test
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
"v.io/jiri"
"v.io/jiri/gitutil"
"v.io/jiri/jiritest"
"v.io/jiri/project"
"v.io/jiri/runutil"
)
func addRemote(t *testing.T, jirix *jiri.X, localProject, name, remoteProject string) {
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("%v", err)
}
defer jirix.NewSeq().Chdir(cwd)
if err := jirix.NewSeq().Chdir(localProject).Done(); err != nil {
t.Fatalf("%v", err)
}
if err := gitutil.New(jirix.NewSeq()).AddRemote(name, remoteProject); err != nil {
t.Fatalf("%v", err)
}
}
func checkReadme(t *testing.T, jirix *jiri.X, project, message string) {
if _, err := jirix.NewSeq().Stat(project); err != nil {
t.Fatalf("%v", err)
}
readmeFile := filepath.Join(project, "README")
data, err := ioutil.ReadFile(readmeFile)
if err != nil {
t.Fatalf("ReadFile(%v) failed: %v", readmeFile, err)
}
if got, want := data, []byte(message); bytes.Compare(got, want) != 0 {
t.Fatalf("unexpected content %v:\ngot\n%s\nwant\n%s\n", project, got, want)
}
}
// Checks that /.jiri/ is ignored in a local project checkout
func checkGitIgnore(t *testing.T, jirix *jiri.X, project string) {
if _, err := jirix.NewSeq().Stat(project); err != nil {
t.Fatalf("%v", err)
}
gitInfoExcludeFile := filepath.Join(project, ".git", "info", "exclude")
data, err := ioutil.ReadFile(gitInfoExcludeFile)
if err != nil {
t.Fatalf("ReadFile(%v) failed: %v", gitInfoExcludeFile, err)
}
excludeString := "/.jiri/"
if !strings.Contains(string(data), excludeString) {
t.Fatalf("Did not find \"%v\" in exclude file", excludeString)
}
}
func createLocalManifestCopy(t *testing.T, jirix *jiri.X, dir, manifestDir string) {
// Load the remote manifest.
m, err := project.ManifestFromFile(jirix, filepath.Join(manifestDir, "v2", "default"))
if err != nil {
t.Fatal(err)
}
// Store the manifest locally.
if err := m.ToFile(jirix, filepath.Join(dir, ".local_manifest")); err != nil {
t.Fatal(err)
}
}
func createLocalManifestStub(t *testing.T, jirix *jiri.X, dir string) {
// Create a manifest stub.
manifest := project.Manifest{}
imp := project.Import{}
imp.Name = "default"
manifest.Imports = append(manifest.Imports, imp)
// Store the manifest locally.
if err := manifest.ToFile(jirix, filepath.Join(dir, ".local_manifest")); err != nil {
t.Fatal(err)
}
}
func commitFile(t *testing.T, jirix *jiri.X, dir, file, msg string) {
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
defer jirix.NewSeq().Chdir(cwd)
if err := jirix.NewSeq().Chdir(dir).Done(); err != nil {
t.Fatal(err)
}
if err := gitutil.New(jirix.NewSeq()).CommitFile(file, msg); err != nil {
t.Fatal(err)
}
}
func createRemoteManifest(t *testing.T, jirix *jiri.X, dir string, remotes []string) {
manifestDir, perm := filepath.Join(dir, "v2"), os.FileMode(0755)
if err := jirix.NewSeq().MkdirAll(manifestDir, perm).Done(); err != nil {
t.Fatalf("%v", err)
}
manifest := project.Manifest{}
for i, remote := range remotes {
project := project.Project{
Name: remote,
Path: localProjectName(i),
Protocol: "git",
Remote: remote,
}
manifest.Projects = append(manifest.Projects, project)
}
commitManifest(t, jirix, &manifest, dir)
}
func commitManifest(t *testing.T, jirix *jiri.X, manifest *project.Manifest, manifestDir string) {
manifestFile := filepath.Join(manifestDir, "v2", "default")
if err := manifest.ToFile(jirix, manifestFile); err != nil {
t.Fatal(err)
}
commitFile(t, jirix, manifestDir, manifestFile, "creating manifest")
}
func createProject(t *testing.T, jirix *jiri.X, manifestDir, name, remote, path string) {
m, err := project.ManifestFromFile(jirix, filepath.Join(manifestDir, "v2", "default"))
if err != nil {
t.Fatal(err)
}
m.Projects = append(m.Projects, project.Project{Name: name, Remote: remote, Path: path})
commitManifest(t, jirix, m, manifestDir)
}
func deleteProject(t *testing.T, jirix *jiri.X, manifestDir, remote string) {
m, err := project.ManifestFromFile(jirix, filepath.Join(manifestDir, "v2", "default"))
if err != nil {
t.Fatal(err)
}
deleteKey := project.MakeProjectKey(remote, remote)
var projects []project.Project
for _, p := range m.Projects {
if p.Key() != deleteKey {
projects = append(projects, p)
}
}
m.Projects = projects
commitManifest(t, jirix, m, manifestDir)
}
// Identify the current revision for a given project.
func currentRevision(t *testing.T, jirix *jiri.X, name string) string {
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("%v", err)
}
defer jirix.NewSeq().Chdir(cwd)
if err := jirix.NewSeq().Chdir(name).Done(); err != nil {
t.Fatalf("%v", err)
}
revision, err := gitutil.New(jirix.NewSeq()).CurrentRevision()
if err != nil {
t.Fatalf("%v", err)
}
return revision
}
// Fix the revision in the manifest file.
func setRevisionForProject(t *testing.T, jirix *jiri.X, manifestDir, name, revision string) {
m, err := project.ManifestFromFile(jirix, filepath.Join(manifestDir, "v2", "default"))
if err != nil {
t.Fatal(err)
}
updated := false
for i, p := range m.Projects {
if p.Name == name {
p.Revision = revision
m.Projects[i] = p
updated = true
break
}
}
if !updated {
t.Fatalf("failed to fix revision for project %v", name)
}
commitManifest(t, jirix, m, manifestDir)
}
func holdProjectBack(t *testing.T, jirix *jiri.X, manifestDir, name string) {
revision := currentRevision(t, jirix, name)
setRevisionForProject(t, jirix, manifestDir, name, revision)
}
func localProjectName(i int) string {
return "test-local-project-" + fmt.Sprintf("%d", i)
}
func moveProject(t *testing.T, jirix *jiri.X, manifestDir, name, dst string) {
m, err := project.ManifestFromFile(jirix, filepath.Join(manifestDir, "v2", "default"))
if err != nil {
t.Fatal(err)
}
updated := false
for i, p := range m.Projects {
if p.Name == name {
p.Path = dst
m.Projects[i] = p
updated = true
break
}
}
if !updated {
t.Fatalf("failed to set path for project %v", name)
}
commitManifest(t, jirix, m, manifestDir)
}
func remoteProjectName(i int) string {
return "test-remote-project-" + fmt.Sprintf("%d", i)
}
func setupNewProject(t *testing.T, jirix *jiri.X, dir, name string, ignore bool) string {
projectDir, perm := filepath.Join(dir, name), os.FileMode(0755)
s := jirix.NewSeq()
if err := s.MkdirAll(projectDir, perm).Done(); err != nil {
t.Fatalf("%v", err)
}
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("%v", err)
}
defer jirix.NewSeq().Chdir(cwd)
if err := s.Chdir(projectDir).Done(); err != nil {
t.Fatalf("%v", err)
}
if err := gitutil.New(jirix.NewSeq()).Init(projectDir); err != nil {
t.Fatalf("%v", err)
}
if ignore {
ignoreFile := filepath.Join(projectDir, ".gitignore")
if err := s.WriteFile(ignoreFile, []byte(jiri.ProjectMetaDir), os.FileMode(0644)).Done(); err != nil {
t.Fatalf("%v", err)
}
if err := gitutil.New(jirix.NewSeq()).Add(ignoreFile); err != nil {
t.Fatalf("%v", err)
}
}
if err := gitutil.New(jirix.NewSeq()).Commit(); err != nil {
t.Fatalf("%v", err)
}
return projectDir
}
func writeEmptyMetadata(t *testing.T, jirix *jiri.X, projectDir string) {
metadataFile := filepath.Join(projectDir, jiri.ProjectMetaDir, jiri.ProjectMetaFile)
p := project.Project{}
if err := p.ToFile(jirix, metadataFile); err != nil {
t.Fatal(err)
}
}
func writeReadme(t *testing.T, jirix *jiri.X, projectDir, message string) {
path, perm := filepath.Join(projectDir, "README"), os.FileMode(0644)
if err := ioutil.WriteFile(path, []byte(message), perm); err != nil {
t.Fatalf("WriteFile(%v, %v) failed: %v", path, perm, err)
}
commitFile(t, jirix, projectDir, path, "creating README")
}
func createAndCheckoutBranch(t *testing.T, jirix *jiri.X, projectDir, branch string) {
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("%v", err)
}
defer jirix.NewSeq().Chdir(cwd)
if err := jirix.NewSeq().Chdir(projectDir).Done(); err != nil {
t.Fatalf("%v", err)
}
if err := gitutil.New(jirix.NewSeq()).CreateAndCheckoutBranch(branch); err != nil {
t.Fatalf("%v", err)
}
}
func resetToOriginMaster(t *testing.T, jirix *jiri.X, projectDir string) {
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("%v", err)
}
defer jirix.NewSeq().Chdir(cwd)
if err := jirix.NewSeq().Chdir(projectDir).Done(); err != nil {
t.Fatalf("%v", err)
}
if err := gitutil.New(jirix.NewSeq()).Reset("origin/master"); err != nil {
t.Fatalf("%v", err)
}
}
func checkProjectsMatchPaths(t *testing.T, gotProjects project.Projects, wantProjectPaths []string) {
gotProjectPaths := []string{}
for _, p := range gotProjects {
gotProjectPaths = append(gotProjectPaths, p.Path)
}
sort.Strings(gotProjectPaths)
sort.Strings(wantProjectPaths)
if !reflect.DeepEqual(gotProjectPaths, wantProjectPaths) {
t.Errorf("project paths got %v, want %v", gotProjectPaths, wantProjectPaths)
}
}
// TestLocalProjects tests the behavior of the LocalProjects method with
// different ScanModes.
func TestLocalProjects(t *testing.T) {
jirix, cleanup := jiritest.NewX(t)
defer cleanup()
// Create some projects.
numProjects, projectPaths := 3, []string{}
for i := 0; i < numProjects; i++ {
name := localProjectName(i)
path := setupNewProject(t, jirix, jirix.Root, name, true)
p := project.Project{
Path: path,
Name: name,
Protocol: "git",
}
if err := project.InternalWriteMetadata(jirix, p, path); err != nil {
t.Fatalf("writeMetadata %v %v) failed: %v\n", p, path, err)
}
projectPaths = append(projectPaths, path)
}
// Create a latest update snapshot but only tell it about the first project.
manifest := project.Manifest{
Projects: []project.Project{
{
Name: projectPaths[0],
Path: localProjectName(0),
Protocol: "git",
Remote: projectPaths[0],
},
},
}
if err := jirix.NewSeq().MkdirAll(jirix.UpdateHistoryDir(), 0755).Done(); err != nil {
t.Fatalf("MkdirAll(%v) failed: %v", jirix.UpdateHistoryDir(), err)
}
if err := manifest.ToFile(jirix, jirix.UpdateHistoryLatestLink()); err != nil {
t.Fatalf("manifest.ToFile(%v) failed: %v", jirix.UpdateHistoryLatestLink(), err)
}
// LocalProjects with scanMode = FastScan should only find the first
// project.
foundProjects, err := project.LocalProjects(jirix, project.FastScan)
if err != nil {
t.Fatalf("LocalProjects(%v) failed: %v", project.FastScan, err)
}
checkProjectsMatchPaths(t, foundProjects, projectPaths[:1])
// LocalProjects with scanMode = FullScan should find all projects.
foundProjects, err = project.LocalProjects(jirix, project.FullScan)
if err != nil {
t.Fatalf("LocalProjects(%v) failed: %v", project.FastScan, err)
}
checkProjectsMatchPaths(t, foundProjects, projectPaths[:])
// Check that deleting a project forces LocalProjects to run a full scan,
// even if FastScan is specified.
if err := jirix.NewSeq().RemoveAll(projectPaths[0]).Done(); err != nil {
t.Fatalf("RemoveAll(%v) failed: %v", projectPaths[0])
}
foundProjects, err = project.LocalProjects(jirix, project.FastScan)
if err != nil {
t.Fatalf("LocalProjects(%v) failed: %v", project.FastScan, err)
}
checkProjectsMatchPaths(t, foundProjects, projectPaths[1:])
}
// TestUpdateUniverse is a comprehensive test of the "jiri update"
// logic that handles projects.
//
// TODO(jsimsa): Add tests for the logic that updates tools.
func TestUpdateUniverse(t *testing.T) {
// Setup an instance of jiri universe, creating the remote repositories for
// the manifest and projects under the ".remote" directory, which is ignored
// from the consideration of LocalProjects().
jirix, cleanup := jiritest.NewX(t)
defer cleanup()
localDir := jirix.Root
remoteDir := filepath.Join(jirix.Root, ".remote")
localManifest := setupNewProject(t, jirix, localDir, ".manifest", false)
writeEmptyMetadata(t, jirix, localManifest)
remoteManifest := setupNewProject(t, jirix, remoteDir, "test-remote-manifest", false)
addRemote(t, jirix, localManifest, "origin", remoteManifest)
numProjects, remoteProjects := 6, []string{}
for i := 0; i < numProjects; i++ {
remoteProject := setupNewProject(t, jirix, remoteDir, remoteProjectName(i), true)
remoteProjects = append(remoteProjects, remoteProject)
}
createRemoteManifest(t, jirix, remoteManifest, remoteProjects)
// Check that calling UpdateUniverse() creates local copies of
// the remote repositories, advancing projects to HEAD or to
// the fixed revision set in the manifest.
for _, remoteProject := range remoteProjects {
writeReadme(t, jirix, remoteProject, "revision 1")
}
holdProjectBack(t, jirix, remoteManifest, remoteProjects[0])
for _, remoteProject := range remoteProjects {
writeReadme(t, jirix, remoteProject, "revision 2")
}
if err := project.UpdateUniverse(jirix, false); err != nil {
t.Fatalf("%v", err)
}
checkCreateFn := func(i int, revision string) {
localProject := filepath.Join(localDir, localProjectName(i))
checkGitIgnore(t, jirix, localProject)
if i == 0 {
checkReadme(t, jirix, localProject, "revision 1")
} else {
checkReadme(t, jirix, localProject, revision)
}
}
for i, _ := range remoteProjects {
checkCreateFn(i, "revision 2")
}
// Commit more work to the remote repositories and check that
// calling UpdateUniverse() advances project to HEAD or to the
// fixed revision set in the manifest.
holdProjectBack(t, jirix, remoteManifest, remoteProjects[1])
for _, remoteProject := range remoteProjects {
writeReadme(t, jirix, remoteProject, "revision 3")
}
if err := project.UpdateUniverse(jirix, false); err != nil {
t.Fatalf("%v", err)
}
checkUpdateFn := func(i int, revision string) {
if i == 1 {
checkReadme(t, jirix, filepath.Join(localDir, localProjectName(i)), "revision 2")
} else {
checkCreateFn(i, revision)
}
}
for i, _ := range remoteProjects {
checkUpdateFn(i, "revision 3")
}
// Create an uncommitted file and make sure UpdateUniverse()
// does not drop it. This ensures that the "git reset --hard"
// mechanism used for pointing the master branch to a fixed
// revision does not lose work in progress.
file, perm, want := filepath.Join(remoteProjects[1], "uncommitted_file"), os.FileMode(0644), []byte("uncommitted work")
if err := ioutil.WriteFile(file, want, perm); err != nil {
t.Fatalf("WriteFile(%v, %v) failed: %v", file, err, perm)
}
if err := project.UpdateUniverse(jirix, false); err != nil {
t.Fatalf("%v", err)
}
got, err := ioutil.ReadFile(file)
if err != nil {
t.Fatalf("%v", err)
}
if bytes.Compare(got, want) != 0 {
t.Fatalf("unexpected content %v:\ngot\n%s\nwant\n%s\n", remoteProjects[1], got, want)
}
// Update the local path at which a remote project is to be
// located and check that UpdateUniverse() moves the local
// copy of the project.
destination := filepath.Join("test", localProjectName(2))
moveProject(t, jirix, remoteManifest, remoteProjects[2], destination)
if err := project.UpdateUniverse(jirix, false); err != nil {
t.Fatalf("%v", err)
}
checkMoveFn := func(i int, revision string) {
if i == 2 {
checkReadme(t, jirix, filepath.Join(localDir, destination), revision)
} else {
checkUpdateFn(i, revision)
}
}
for i, _ := range remoteProjects {
checkMoveFn(i, "revision 3")
}
// Delete a remote project and check that UpdateUniverse()
// deletes the local copy of the project.
deleteProject(t, jirix, remoteManifest, remoteProjects[3])
if err := project.UpdateUniverse(jirix, true); err != nil {
t.Fatalf("%v", err)
}
checkDeleteFn := func(i int, revision string) {
if i == 3 {
localProject := filepath.Join(localDir, localProjectName(i))
if _, err := jirix.NewSeq().Stat(localProject); err == nil {
t.Fatalf("project %v has not been deleted", localProject)
} else {
if !runutil.IsNotExist(err) {
t.Fatalf("%v", err)
}
}
} else {
checkMoveFn(i, revision)
}
}
for i, _ := range remoteProjects {
checkDeleteFn(i, "revision 3")
}
// Delete a project and create a new one with a different name but the same
// path. Check that UpdateUniverse() does not fail.
deleteProject(t, jirix, remoteManifest, remoteProjects[4])
createProject(t, jirix, remoteManifest, "new.project", remoteProjects[4], localProjectName(4))
if err := project.UpdateUniverse(jirix, true); err != nil {
t.Fatalf("%v", err)
}
// Commit to a non-master branch of a remote project and check that
// UpdateUniverse() can update the local project to point to a revision on
// that branch.
writeReadme(t, jirix, remoteProjects[5], "master commit")
createAndCheckoutBranch(t, jirix, remoteProjects[5], "non_master")
writeReadme(t, jirix, remoteProjects[5], "non master commit")
remoteBranchRevision := currentRevision(t, jirix, remoteProjects[5])
setRevisionForProject(t, jirix, remoteManifest, remoteProjects[5], remoteBranchRevision)
if err := project.UpdateUniverse(jirix, true); err != nil {
t.Fatalf("%v", err)
}
localProject := filepath.Join(localDir, localProjectName(5))
localBranchRevision := currentRevision(t, jirix, localProject)
if localBranchRevision != remoteBranchRevision {
t.Fatalf("project 5 is at revision %v, expected %v\n", localBranchRevision, remoteBranchRevision)
}
// Reset back to origin/master so the next update without a "revision" works.
resetToOriginMaster(t, jirix, localProject)
// Create a local manifest that imports the remote manifest
// and check that UpdateUniverse() has no effect.
createLocalManifestStub(t, jirix, localDir)
if err := project.UpdateUniverse(jirix, true); err != nil {
t.Fatalf("%v", err)
}
checkRebaseFn := func(i int, revision string) {
if i == 5 {
checkReadme(t, jirix, localProject, "non master commit")
} else {
checkDeleteFn(i, revision)
}
}
for i, _ := range remoteProjects {
checkRebaseFn(i, "revision 3")
}
// Create a local manifest that matches the remote manifest,
// then revert the remote manifest to its initial version and
// check that UpdateUniverse() has no effect.
createLocalManifestCopy(t, jirix, localDir, remoteManifest)
createRemoteManifest(t, jirix, remoteManifest, remoteProjects)
if err := project.UpdateUniverse(jirix, true); err != nil {
t.Fatalf("%v", err)
}
for i, _ := range remoteProjects {
checkRebaseFn(i, "revision 3")
}
}
func TestFileImportCycle(t *testing.T) {
jirix, cleanup := jiritest.NewX(t)
defer cleanup()
// Set up the cycle .jiri_manifest -> A -> B -> A
jiriManifest := project.Manifest{
LocalImports: []project.LocalImport{
{File: "A"},
},
}
manifestA := project.Manifest{
LocalImports: []project.LocalImport{
{File: "B"},
},
}
manifestB := project.Manifest{
LocalImports: []project.LocalImport{
{File: "A"},
},
}
if err := jiriManifest.ToFile(jirix, jirix.JiriManifestFile()); err != nil {
t.Fatal(err)
}
if err := manifestA.ToFile(jirix, filepath.Join(jirix.Root, "A")); err != nil {
t.Fatal(err)
}
if err := manifestB.ToFile(jirix, filepath.Join(jirix.Root, "B")); err != nil {
t.Fatal(err)
}
// The update should complain about the cycle.
err := project.UpdateUniverse(jirix, false)
if got, want := fmt.Sprint(err), "import cycle detected in local manifest files"; !strings.Contains(got, want) {
t.Errorf("got error %v, want substr %v", got, want)
}
}
func TestRemoteImportCycle(t *testing.T) {
jirix, cleanup := jiritest.NewX(t)
defer cleanup()
remoteDir := filepath.Join(jirix.Root, ".remote")
// Set up two remote manifest projects, remote1 and remote2.
remote1 := setupNewProject(t, jirix, remoteDir, "remote1", true)
remote2 := setupNewProject(t, jirix, remoteDir, "remote2", true)
fileA, fileB := filepath.Join(remote1, "A"), filepath.Join(remote2, "B")
// Set up the cycle .jiri_manifest -> remote1+A -> remote2+B -> remote1+A
jiriManifest := project.Manifest{
Imports: []project.Import{
{Manifest: "A", Name: "n1", Remote: remote1},
},
}
manifestA := project.Manifest{
Imports: []project.Import{
{Manifest: "B", Name: "n2", Remote: remote2},
},
}
manifestB := project.Manifest{
Imports: []project.Import{
{Manifest: "A", Name: "n3", Remote: remote1},
},
}
if err := jiriManifest.ToFile(jirix, jirix.JiriManifestFile()); err != nil {
t.Fatal(err)
}
if err := manifestA.ToFile(jirix, fileA); err != nil {
t.Fatal(err)
}
if err := manifestB.ToFile(jirix, fileB); err != nil {
t.Fatal(err)
}
commitFile(t, jirix, remote1, fileA, "commit A")
commitFile(t, jirix, remote2, fileB, "commit B")
// The update should complain about the cycle.
err := project.UpdateUniverse(jirix, false)
if got, want := fmt.Sprint(err), "import cycle detected in remote manifest imports"; !strings.Contains(got, want) {
t.Errorf("got error %v, want substr %v", got, want)
}
}
func TestFileAndRemoteImportCycle(t *testing.T) {
jirix, cleanup := jiritest.NewX(t)
defer cleanup()
remoteDir := filepath.Join(jirix.Root, ".remote")
// Set up two remote manifest projects, remote1 and remote2.
remote1 := setupNewProject(t, jirix, remoteDir, "remote1", true)
remote2 := setupNewProject(t, jirix, remoteDir, "remote2", true)
fileA, fileD := filepath.Join(remote1, "A"), filepath.Join(remote1, "D")
fileB, fileC := filepath.Join(remote2, "B"), filepath.Join(remote2, "C")
// Set up the cycle .jiri_manifest -> remote1+A -> remote2+B -> C -> remote1+D -> A
jiriManifest := project.Manifest{
Imports: []project.Import{
{Manifest: "A", Root: "r1", Name: "n1", Remote: remote1},
},
}
manifestA := project.Manifest{
Imports: []project.Import{
{Manifest: "B", Root: "r2", Name: "n2", Remote: remote2},
},
}
manifestB := project.Manifest{
LocalImports: []project.LocalImport{
{File: "C"},
},
}
manifestC := project.Manifest{
Imports: []project.Import{
{Manifest: "D", Root: "r3", Name: "n3", Remote: remote1},
},
}
manifestD := project.Manifest{
LocalImports: []project.LocalImport{
{File: "A"},
},
}
if err := jiriManifest.ToFile(jirix, jirix.JiriManifestFile()); err != nil {
t.Fatal(err)
}
if err := manifestA.ToFile(jirix, fileA); err != nil {
t.Fatal(err)
}
if err := manifestB.ToFile(jirix, fileB); err != nil {
t.Fatal(err)
}
if err := manifestC.ToFile(jirix, fileC); err != nil {
t.Fatal(err)
}
if err := manifestD.ToFile(jirix, fileD); err != nil {
t.Fatal(err)
}
commitFile(t, jirix, remote1, fileA, "commit A")
commitFile(t, jirix, remote2, fileB, "commit B")
commitFile(t, jirix, remote2, fileC, "commit C")
commitFile(t, jirix, remote1, fileD, "commit D")
// The update should complain about the cycle.
err := project.UpdateUniverse(jirix, false)
if got, want := fmt.Sprint(err), "import cycle detected"; !strings.Contains(got, want) {
t.Errorf("got error %v, want substr %v", got, want)
}
}
// TestUnsupportedProtocolErr checks that calling
// UnsupportedPrototoclErr.Error() does not result in an infinite loop.
func TestUnsupportedPrototocolErr(t *testing.T) {
err := project.UnsupportedProtocolErr("foo")
_ = err.Error()
}
type binDirTest struct {
Name string
Setup func(old, new string) error
Teardown func(old, new string) error
Error string
CheckBackup bool
}
func TestTransitionBinDir(t *testing.T) {
tests := []binDirTest{
{
"No old dir",
func(old, new string) error { return nil },
nil,
"",
false,
},
{
"Empty old dir",
func(old, new string) error {
return os.MkdirAll(old, 0777)
},
nil,
"",
true,
},
{
"Populated old dir",
func(old, new string) error {
if err := os.MkdirAll(old, 0777); err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(old, "tool"), []byte("foo"), 0777)
},
nil,
"",
true,
},
{
"Symlinked old dir",
func(old, new string) error {
if err := os.MkdirAll(filepath.Dir(old), 0777); err != nil {
return err
}
return os.Symlink(new, old)
},
nil,
"",
false,
},
{
"Symlinked old dir pointing elsewhere",
func(old, new string) error {
if err := os.MkdirAll(filepath.Dir(old), 0777); err != nil {
return err
}
return os.Symlink(filepath.Dir(new), old)
},
nil,
"",
true,
},
{
"Unreadable old dir parent",
func(old, new string) error {
if err := os.MkdirAll(old, 0777); err != nil {
return err
}
return os.Chmod(filepath.Dir(old), 0222)
},
func(old, new string) error {
return os.Chmod(filepath.Dir(old), 0777)
},
"Failed to stat old bin dir",
false,
},
{
"Unwritable old dir",
func(old, new string) error {
if err := os.MkdirAll(old, 0777); err != nil {
return err
}
return os.Chmod(old, 0444)
},
func(old, new string) error {
return os.Chmod(old, 0777)
},
"Failed to backup old bin dir",
false,
},
{
"Unreadable backup dir parent",
func(old, new string) error {
if err := os.MkdirAll(old, 0777); err != nil {
return err
}
return os.Chmod(filepath.Dir(new), 0222)
},
func(old, new string) error {
return os.Chmod(filepath.Dir(new), 0777)
},
"Failed to stat backup bin dir",
false,
},
{
"Existing backup dir",
func(old, new string) error {
if err := os.MkdirAll(old, 0777); err != nil {
return err
}
return os.MkdirAll(new+".BACKUP", 0777)
},
nil,
"Backup bin dir",
false,
},
}
for _, test := range tests {
jirix, cleanup := jiritest.NewX(t)
if err := testTransitionBinDir(jirix, test); err != nil {
t.Errorf("%s: %v", test.Name, err)
}
cleanup()
}
}
func testTransitionBinDir(jirix *jiri.X, test binDirTest) (e error) {
oldDir, newDir := filepath.Join(jirix.Root, "devtools", "bin"), jirix.BinDir()
// The new bin dir always exists.
if err := os.MkdirAll(newDir, 0777); err != nil {
return fmt.Errorf("make new dir failed: %v", err)
}
if err := test.Setup(oldDir, newDir); err != nil {
return fmt.Errorf("setup failed: %v", err)
}
if test.Teardown != nil {
defer func() {
if err := test.Teardown(oldDir, newDir); err != nil && e == nil {
e = fmt.Errorf("teardown failed: %v", err)
}
}()
}
oldInfo, _ := os.Stat(oldDir)
switch err := project.TransitionBinDir(jirix); {
case err != nil && test.Error == "":
return fmt.Errorf("got error %q, want success", err)
case err != nil && !strings.Contains(fmt.Sprint(err), test.Error):
return fmt.Errorf("got error %q, want prefix %q", err, test.Error)
case err == nil && test.Error != "":
return fmt.Errorf("got no error, want %q", test.Error)
case err == nil && test.Error == "":
// Make sure the symlink exists and is correctly linked.
link, err := os.Readlink(oldDir)
if err != nil {
return fmt.Errorf("old dir isn't a symlink: %v", err)
}
if got, want := link, newDir; got != want {
return fmt.Errorf("old dir symlink got %v, want %v", got, want)
}
if test.CheckBackup {
// Make sure the oldDir was backed up correctly.
backupDir := filepath.Join(jirix.RootMetaDir(), "bin.BACKUP")
backupInfo, err := os.Stat(backupDir)
if err != nil {
return fmt.Errorf("stat backup dir failed: %v", err)
}
if !os.SameFile(oldInfo, backupInfo) {
return fmt.Errorf("old dir wasn't backed up correctly")
}
}
}
return nil
}
func TestManifestToFromBytes(t *testing.T) {
tests := []struct {
Manifest project.Manifest
XML string
}{
{
project.Manifest{},
`<manifest>
</manifest>
`,
},
{
project.Manifest{
Imports: []project.Import{
{
Manifest: "manifest1",
Name: "remoteimport1",
Protocol: "git",
Remote: "remote1",
RemoteBranch: "master",
},
{
Manifest: "manifest2",
Name: "remoteimport2",
Protocol: "git",
Remote: "remote2",
RemoteBranch: "branch2",
},
{
Name: "oldimport",
},
},
LocalImports: []project.LocalImport{
{File: "fileimport"},
},
Projects: []project.Project{
{
Name: "project1",
Path: "path1",
Protocol: "git",
Remote: "remote1",
RemoteBranch: "master",
Revision: "HEAD",
GerritHost: "https://test-review.googlesource.com",
GitHooks: "path/to/githooks",
RunHook: "path/to/hook",
},
{
Name: "project2",
Path: "path2",
Protocol: "git",
Remote: "remote2",
RemoteBranch: "branch2",
Revision: "rev2",
},
},
Tools: []project.Tool{
{
Data: "tooldata",
Name: "tool",
Project: "toolproject",
},
},
},
`<manifest>
<imports>
<import manifest="manifest1" name="remoteimport1" remote="remote1"/>
<import manifest="manifest2" name="remoteimport2" remote="remote2" remotebranch="branch2"/>
<import name="oldimport"/>
<localimport file="fileimport"/>
</imports>
<projects>
<project name="project1" path="path1" remote="remote1" gerrithost="https://test-review.googlesource.com" githooks="path/to/githooks" runhook="path/to/hook"/>
<project name="project2" path="path2" remote="remote2" remotebranch="branch2" revision="rev2"/>
</projects>
<tools>
<tool data="tooldata" name="tool" project="toolproject"/>
</tools>
</manifest>
`,
},
}
for _, test := range tests {
gotBytes, err := test.Manifest.ToBytes()
if err != nil {
t.Errorf("%+v ToBytes failed: %v", test.Manifest, err)
}
if got, want := string(gotBytes), test.XML; got != want {
t.Errorf("%+v ToBytes GOT\n%v\nWANT\n%v", test.Manifest, got, want)
}
manifest, err := project.ManifestFromBytes([]byte(test.XML))
if err != nil {
t.Errorf("%+v FromBytes failed: %v", test.Manifest, err)
}
if got, want := manifest, &test.Manifest; !reflect.DeepEqual(got, want) {
t.Errorf("%+v FromBytes got %#v, want %#v", test.Manifest, got, want)
}
}
}
func TestProjectToFromFile(t *testing.T) {
jirix, cleanup := jiritest.NewX(t)
defer cleanup()
tests := []struct {
Project project.Project
XML string
}{
{
// Default fields are dropped when marshaled, and added when unmarshaled.
project.Project{
Name: "project1",
Path: filepath.Join(jirix.Root, "path1"),
Protocol: "git",
Remote: "remote1",
RemoteBranch: "master",
Revision: "HEAD",
},
`<project name="project1" path="path1" remote="remote1"/>
`,
},
{
project.Project{
Name: "project2",
Path: filepath.Join(jirix.Root, "path2"),
GitHooks: filepath.Join(jirix.Root, "git-hooks"),
RunHook: filepath.Join(jirix.Root, "run-hook"),
Protocol: "git",
Remote: "remote2",
RemoteBranch: "branch2",
Revision: "rev2",
},
`<project name="project2" path="path2" remote="remote2" remotebranch="branch2" revision="rev2" githooks="git-hooks" runhook="run-hook"/>
`,
},
}
for index, test := range tests {
filename := filepath.Join(jirix.Root, fmt.Sprintf("test-%d", index))
if err := test.Project.ToFile(jirix, filename); err != nil {
t.Errorf("%+v ToFile failed: %v", test.Project, err)
}
gotBytes, err := jirix.NewSeq().ReadFile(filename)
if err != nil {
t.Errorf("%+v ReadFile failed: %v", test.Project, err)
}
if got, want := string(gotBytes), test.XML; got != want {
t.Errorf("%+v ToFile GOT\n%v\nWANT\n%v", test.Project, got, want)
}
project, err := project.ProjectFromFile(jirix, filename)
if err != nil {
t.Errorf("%+v FromFile failed: %v", test.Project, err)
}
if got, want := project, &test.Project; !reflect.DeepEqual(got, want) {
t.Errorf("%+v FromFile got %#v, want %#v", test.Project, got, want)
}
}
}
func TestProjectFromFileBackwardsCompatible(t *testing.T) {
jirix, cleanup := jiritest.NewX(t)
defer cleanup()
tests := []struct {
XML string
Project project.Project
}{
// Make sure <Project> opening tag is accepted.
{
`<Project name="project" path="path" remote="remote"/>`,
project.Project{
Name: "project",
Path: filepath.Join(jirix.Root, "path"),
Protocol: "git",
Remote: "remote",
RemoteBranch: "master",
Revision: "HEAD",
},
},
// Make sure <Project> opening and closing tags are accepted.
{
`<Project name="project" path="path" remote="remote"></Project>`,
project.Project{
Name: "project",
Path: filepath.Join(jirix.Root, "path"),
Protocol: "git",
Remote: "remote",
RemoteBranch: "master",
Revision: "HEAD",
},
},
// Make sure "this_attribute_should_be_ignored" is silently ignored.
{
`<Project this_attribute_should_be_ignored="junk" name="project" path="path" remote="remote" remotebranch="branch" revision="rev"></Project>`,
project.Project{
Name: "project",
Path: filepath.Join(jirix.Root, "path"),
Protocol: "git",
Remote: "remote",
RemoteBranch: "branch",
Revision: "rev",
},
},
}
for index, test := range tests {
filename := filepath.Join(jirix.Root, fmt.Sprintf("test-%d", index))
if err := jirix.NewSeq().WriteFile(filename, []byte(test.XML), 0644).Done(); err != nil {
t.Errorf("%+v WriteFile failed: %v", test.Project, err)
}
project, err := project.ProjectFromFile(jirix, filename)
if err != nil {
t.Errorf("%+v FromFile failed: %v", test.Project, err)
}
if got, want := project, &test.Project; !reflect.DeepEqual(got, want) {
t.Errorf("%+v FromFile got %#v, want %#v", test.Project, got, want)
}
}
}