v.io/jiri@v0.0.0-20160715023856-abfb8b131290/project/project_test.go (about)

     1  // Copyright 2015 The Vanadium Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package project_test
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"os"
    12  	"path/filepath"
    13  	"reflect"
    14  	"sort"
    15  	"strings"
    16  	"testing"
    17  
    18  	"v.io/jiri"
    19  	"v.io/jiri/gitutil"
    20  	"v.io/jiri/jiritest"
    21  	"v.io/jiri/project"
    22  )
    23  
    24  func checkReadme(t *testing.T, jirix *jiri.X, p project.Project, message string) {
    25  	if _, err := jirix.NewSeq().Stat(p.Path); err != nil {
    26  		t.Fatalf("%v", err)
    27  	}
    28  	readmeFile := filepath.Join(p.Path, "README")
    29  	data, err := ioutil.ReadFile(readmeFile)
    30  	if err != nil {
    31  		t.Fatalf("ReadFile(%v) failed: %v", readmeFile, err)
    32  	}
    33  	if got, want := data, []byte(message); bytes.Compare(got, want) != 0 {
    34  		t.Fatalf("unexpected content in project %v:\ngot\n%s\nwant\n%s\n", p.Name, got, want)
    35  	}
    36  }
    37  
    38  // Checks that /.jiri/ is ignored in a local project checkout
    39  func checkMetadataIsIgnored(t *testing.T, jirix *jiri.X, p project.Project) {
    40  	if _, err := jirix.NewSeq().Stat(p.Path); err != nil {
    41  		t.Fatalf("%v", err)
    42  	}
    43  	gitInfoExcludeFile := filepath.Join(p.Path, ".git", "info", "exclude")
    44  	data, err := ioutil.ReadFile(gitInfoExcludeFile)
    45  	if err != nil {
    46  		t.Fatalf("ReadFile(%v) failed: %v", gitInfoExcludeFile, err)
    47  	}
    48  	excludeString := "/.jiri/"
    49  	if !strings.Contains(string(data), excludeString) {
    50  		t.Fatalf("Did not find \"%v\" in exclude file", excludeString)
    51  	}
    52  }
    53  
    54  func commitFile(t *testing.T, jirix *jiri.X, dir, file, msg string) {
    55  	cwd, err := os.Getwd()
    56  	if err != nil {
    57  		t.Fatal(err)
    58  	}
    59  	defer jirix.NewSeq().Chdir(cwd)
    60  	if err := jirix.NewSeq().Chdir(dir).Done(); err != nil {
    61  		t.Fatal(err)
    62  	}
    63  	if err := gitutil.New(jirix.NewSeq()).CommitFile(file, msg); err != nil {
    64  		t.Fatal(err)
    65  	}
    66  }
    67  
    68  func projectName(i int) string {
    69  	return fmt.Sprintf("project-%d", i)
    70  }
    71  
    72  func writeReadme(t *testing.T, jirix *jiri.X, projectDir, message string) {
    73  	path, perm := filepath.Join(projectDir, "README"), os.FileMode(0644)
    74  	if err := ioutil.WriteFile(path, []byte(message), perm); err != nil {
    75  		t.Fatalf("WriteFile(%v, %v) failed: %v", path, perm, err)
    76  	}
    77  	commitFile(t, jirix, projectDir, path, "creating README")
    78  }
    79  
    80  func checkProjectsMatchPaths(t *testing.T, gotProjects project.Projects, wantProjectPaths []string) {
    81  	gotProjectPaths := []string{}
    82  	for _, p := range gotProjects {
    83  		gotProjectPaths = append(gotProjectPaths, p.Path)
    84  	}
    85  	sort.Strings(gotProjectPaths)
    86  	sort.Strings(wantProjectPaths)
    87  	if !reflect.DeepEqual(gotProjectPaths, wantProjectPaths) {
    88  		t.Errorf("project paths got %v, want %v", gotProjectPaths, wantProjectPaths)
    89  	}
    90  }
    91  
    92  // TestLocalProjects tests the behavior of the LocalProjects method with
    93  // different ScanModes.
    94  func TestLocalProjects(t *testing.T) {
    95  	jirix, cleanup := jiritest.NewX(t)
    96  	defer cleanup()
    97  
    98  	// Create some projects.
    99  	numProjects, projectPaths := 3, []string{}
   100  	for i := 0; i < numProjects; i++ {
   101  		s := jirix.NewSeq()
   102  		name := projectName(i)
   103  		path := filepath.Join(jirix.Root, name)
   104  		if err := s.MkdirAll(path, 0755).Done(); err != nil {
   105  			t.Fatal(err)
   106  		}
   107  
   108  		// Initialize empty git repository.  The commit is necessary, otherwise
   109  		// "git rev-parse master" fails.
   110  		git := gitutil.New(s, gitutil.RootDirOpt(path))
   111  		if err := git.Init(path); err != nil {
   112  			t.Fatal(err)
   113  		}
   114  		if err := git.Commit(); err != nil {
   115  			t.Fatal(err)
   116  		}
   117  
   118  		// Write project metadata.
   119  		p := project.Project{
   120  			Path: path,
   121  			Name: name,
   122  		}
   123  		if err := project.InternalWriteMetadata(jirix, p, path); err != nil {
   124  			t.Fatalf("writeMetadata %v %v) failed: %v\n", p, path, err)
   125  		}
   126  		projectPaths = append(projectPaths, path)
   127  	}
   128  
   129  	// Create a latest update snapshot but only tell it about the first project.
   130  	manifest := project.Manifest{
   131  		Projects: []project.Project{
   132  			{
   133  				Name: projectName(0),
   134  				Path: projectPaths[0],
   135  			},
   136  		},
   137  	}
   138  	if err := jirix.NewSeq().MkdirAll(jirix.UpdateHistoryDir(), 0755).Done(); err != nil {
   139  		t.Fatalf("MkdirAll(%v) failed: %v", jirix.UpdateHistoryDir(), err)
   140  	}
   141  	if err := manifest.ToFile(jirix, jirix.UpdateHistoryLatestLink()); err != nil {
   142  		t.Fatalf("manifest.ToFile(%v) failed: %v", jirix.UpdateHistoryLatestLink(), err)
   143  	}
   144  
   145  	// LocalProjects with scanMode = FastScan should only find the first
   146  	// project.
   147  	foundProjects, err := project.LocalProjects(jirix, project.FastScan)
   148  	if err != nil {
   149  		t.Fatalf("LocalProjects(%v) failed: %v", project.FastScan, err)
   150  	}
   151  	checkProjectsMatchPaths(t, foundProjects, projectPaths[:1])
   152  
   153  	// LocalProjects with scanMode = FullScan should find all projects.
   154  	foundProjects, err = project.LocalProjects(jirix, project.FullScan)
   155  	if err != nil {
   156  		t.Fatalf("LocalProjects(%v) failed: %v", project.FastScan, err)
   157  	}
   158  	checkProjectsMatchPaths(t, foundProjects, projectPaths[:])
   159  
   160  	// Check that deleting a project forces LocalProjects to run a full scan,
   161  	// even if FastScan is specified.
   162  	if err := jirix.NewSeq().RemoveAll(projectPaths[0]).Done(); err != nil {
   163  		t.Fatalf("RemoveAll(%v) failed: %v", projectPaths[0])
   164  	}
   165  	foundProjects, err = project.LocalProjects(jirix, project.FastScan)
   166  	if err != nil {
   167  		t.Fatalf("LocalProjects(%v) failed: %v", project.FastScan, err)
   168  	}
   169  	checkProjectsMatchPaths(t, foundProjects, projectPaths[1:])
   170  }
   171  
   172  // setupUniverse creates a fake jiri root with 3 remote projects.  Each project
   173  // has a README with text "initial readme".
   174  func setupUniverse(t *testing.T) ([]project.Project, *jiritest.FakeJiriRoot, func()) {
   175  	fake, cleanup := jiritest.NewFakeJiriRoot(t)
   176  	success := false
   177  	defer func() {
   178  		if !success {
   179  			cleanup()
   180  		}
   181  	}()
   182  
   183  	// Create some projects and add them to the remote manifest.
   184  	numProjects := 3
   185  	localProjects := []project.Project{}
   186  	for i := 0; i < numProjects; i++ {
   187  		name := projectName(i)
   188  		path := fmt.Sprintf("path-%d", i)
   189  		if err := fake.CreateRemoteProject(name); err != nil {
   190  			t.Fatal(err)
   191  		}
   192  		p := project.Project{
   193  			Name:   name,
   194  			Path:   filepath.Join(fake.X.Root, path),
   195  			Remote: fake.Projects[name],
   196  		}
   197  		localProjects = append(localProjects, p)
   198  		if err := fake.AddProject(p); err != nil {
   199  			t.Fatal(err)
   200  		}
   201  	}
   202  
   203  	// Create initial commit in each repo.
   204  	for _, remoteProjectDir := range fake.Projects {
   205  		writeReadme(t, fake.X, remoteProjectDir, "initial readme")
   206  	}
   207  
   208  	success = true
   209  	return localProjects, fake, cleanup
   210  }
   211  
   212  // TestUpdateUniverseSimple tests that UpdateUniverse will pull remote projects
   213  // locally, and that jiri metadata is ignored in the repos.
   214  func TestUpdateUniverseSimple(t *testing.T) {
   215  	localProjects, fake, cleanup := setupUniverse(t)
   216  	defer cleanup()
   217  	s := fake.X.NewSeq()
   218  
   219  	// Check that calling UpdateUniverse() creates local copies of the remote
   220  	// repositories, and that jiri metadata is ignored by git.
   221  	if err := fake.UpdateUniverse(false); err != nil {
   222  		t.Fatal(err)
   223  	}
   224  	for _, p := range localProjects {
   225  		if err := s.AssertDirExists(p.Path).Done(); err != nil {
   226  			t.Fatalf("expected project to exist at path %q but none found", p.Path)
   227  		}
   228  		checkReadme(t, fake.X, p, "initial readme")
   229  		checkMetadataIsIgnored(t, fake.X, p)
   230  	}
   231  }
   232  
   233  // TestUpdateUniverseWithRevision checks that UpdateUniverse will pull remote
   234  // projects at the specified revision.
   235  func TestUpdateUniverseWithRevision(t *testing.T) {
   236  	localProjects, fake, cleanup := setupUniverse(t)
   237  	defer cleanup()
   238  	s := fake.X.NewSeq()
   239  
   240  	// Set project 1's revision in the manifest to the current revision.
   241  	git := gitutil.New(s, gitutil.RootDirOpt(fake.Projects[localProjects[1].Name]))
   242  	rev, err := git.CurrentRevision()
   243  	if err != nil {
   244  		t.Fatal(err)
   245  	}
   246  	m, err := fake.ReadRemoteManifest()
   247  	if err != nil {
   248  		t.Fatal(err)
   249  	}
   250  	projects := []project.Project{}
   251  	for _, p := range m.Projects {
   252  		if p.Name == localProjects[1].Name {
   253  			p.Revision = rev
   254  		}
   255  		projects = append(projects, p)
   256  	}
   257  	m.Projects = projects
   258  	if err := fake.WriteRemoteManifest(m); err != nil {
   259  		t.Fatal(err)
   260  	}
   261  	// Update README in all projects.
   262  	for _, remoteProjectDir := range fake.Projects {
   263  		writeReadme(t, fake.X, remoteProjectDir, "new revision")
   264  	}
   265  	// Check that calling UpdateUniverse() updates all projects except for
   266  	// project 1.
   267  	if err := fake.UpdateUniverse(false); err != nil {
   268  		t.Fatal(err)
   269  	}
   270  	for i, p := range localProjects {
   271  		if i == 1 {
   272  			checkReadme(t, fake.X, p, "initial readme")
   273  		} else {
   274  			checkReadme(t, fake.X, p, "new revision")
   275  		}
   276  	}
   277  }
   278  
   279  // TestUpdateUniverseWithUncommitted checks that uncommitted files are not droped
   280  // by UpdateUniverse(). This ensures that the "git reset --hard" mechanism used
   281  // for pointing the master branch to a fixed revision does not lose work in
   282  // progress.
   283  func TestUpdateUniverseWithUncommitted(t *testing.T) {
   284  	localProjects, fake, cleanup := setupUniverse(t)
   285  	defer cleanup()
   286  	if err := fake.UpdateUniverse(false); err != nil {
   287  		t.Fatal(err)
   288  	}
   289  
   290  	// Create an uncommitted file in project 1.
   291  	file, perm, want := filepath.Join(localProjects[1].Path, "uncommitted_file"), os.FileMode(0644), []byte("uncommitted work")
   292  	if err := ioutil.WriteFile(file, want, perm); err != nil {
   293  		t.Fatalf("WriteFile(%v, %v) failed: %v", file, err, perm)
   294  	}
   295  	if err := fake.UpdateUniverse(false); err != nil {
   296  		t.Fatal(err)
   297  	}
   298  	got, err := ioutil.ReadFile(file)
   299  	if err != nil {
   300  		t.Fatalf("%v", err)
   301  	}
   302  	if bytes.Compare(got, want) != 0 {
   303  		t.Fatalf("unexpected content %v:\ngot\n%s\nwant\n%s\n", localProjects[1], got, want)
   304  	}
   305  }
   306  
   307  // TestUpdateUniverseMovedProject checks that UpdateUniverse can move a
   308  // project.
   309  func TestUpdateUniverseMovedProject(t *testing.T) {
   310  	localProjects, fake, cleanup := setupUniverse(t)
   311  	defer cleanup()
   312  	s := fake.X.NewSeq()
   313  	if err := fake.UpdateUniverse(false); err != nil {
   314  		t.Fatal(err)
   315  	}
   316  
   317  	// Update the local path at which project 1 is located.
   318  	m, err := fake.ReadRemoteManifest()
   319  	if err != nil {
   320  		t.Fatal(err)
   321  	}
   322  	oldProjectPath := localProjects[1].Path
   323  	localProjects[1].Path = filepath.Join(fake.X.Root, "new-project-path")
   324  	projects := []project.Project{}
   325  	for _, p := range m.Projects {
   326  		if p.Name == localProjects[1].Name {
   327  			p.Path = localProjects[1].Path
   328  		}
   329  		projects = append(projects, p)
   330  	}
   331  	m.Projects = projects
   332  	if err := fake.WriteRemoteManifest(m); err != nil {
   333  		t.Fatal(err)
   334  	}
   335  	// Check that UpdateUniverse() moves the local copy of the project 1.
   336  	if err := fake.UpdateUniverse(false); err != nil {
   337  		t.Fatal(err)
   338  	}
   339  	if err := s.AssertDirExists(oldProjectPath).Done(); err == nil {
   340  		t.Fatalf("expected project %q at path %q not to exist but it did", localProjects[1].Name, oldProjectPath)
   341  	}
   342  	if err := s.AssertDirExists(localProjects[2].Path).Done(); err != nil {
   343  		t.Fatalf("expected project %q at path %q to exist but it did not", localProjects[1].Name, localProjects[1].Path)
   344  	}
   345  	checkReadme(t, fake.X, localProjects[1], "initial readme")
   346  }
   347  
   348  // TestUpdateUniverseDeletedProject checks that UpdateUniverse will delete a
   349  // project iff gc=true.
   350  func TestUpdateUniverseDeletedProject(t *testing.T) {
   351  	localProjects, fake, cleanup := setupUniverse(t)
   352  	defer cleanup()
   353  	s := fake.X.NewSeq()
   354  	if err := fake.UpdateUniverse(false); err != nil {
   355  		t.Fatal(err)
   356  	}
   357  
   358  	// Delete project 1.
   359  	m, err := fake.ReadRemoteManifest()
   360  	if err != nil {
   361  		t.Fatal(err)
   362  	}
   363  	projects := []project.Project{}
   364  	for _, p := range m.Projects {
   365  		if p.Name == localProjects[1].Name {
   366  			continue
   367  		}
   368  		projects = append(projects, p)
   369  	}
   370  	m.Projects = projects
   371  	if err := fake.WriteRemoteManifest(m); err != nil {
   372  		t.Fatal(err)
   373  	}
   374  	// Check that UpdateUniverse() with gc=false does not delete the local copy
   375  	// of the project.
   376  	if err := fake.UpdateUniverse(false); err != nil {
   377  		t.Fatal(err)
   378  	}
   379  	if err := s.AssertDirExists(localProjects[1].Path).Done(); err != nil {
   380  		t.Fatalf("expected project %q at path %q to exist but it did not", localProjects[1].Name, localProjects[1].Path)
   381  	}
   382  	checkReadme(t, fake.X, localProjects[1], "initial readme")
   383  	// Check that UpdateUniverse() with gc=true does delete the local copy of
   384  	// the project.
   385  	if err := fake.UpdateUniverse(true); err != nil {
   386  		t.Fatal(err)
   387  	}
   388  	if err := s.AssertDirExists(localProjects[1].Path).Done(); err == nil {
   389  		t.Fatalf("expected project %q at path %q not to exist but it did", localProjects[1].Name, localProjects[3].Path)
   390  	}
   391  }
   392  
   393  // TestUpdateUniverseNewProjectSamePath checks that UpdateUniverse can handle a
   394  // new project with the same path as a deleted project, but a different path.
   395  func TestUpdateUniverseNewProjectSamePath(t *testing.T) {
   396  	localProjects, fake, cleanup := setupUniverse(t)
   397  	defer cleanup()
   398  	if err := fake.UpdateUniverse(false); err != nil {
   399  		t.Fatal(err)
   400  	}
   401  
   402  	// Delete a project 1 and create a new one with a different name but the
   403  	// same path.
   404  	m, err := fake.ReadRemoteManifest()
   405  	if err != nil {
   406  		t.Fatal(err)
   407  	}
   408  	newProjectName := "new-project-name"
   409  	projects := []project.Project{}
   410  	for _, p := range m.Projects {
   411  		if p.Path == localProjects[1].Path {
   412  			p.Name = newProjectName
   413  		}
   414  		projects = append(projects, p)
   415  	}
   416  	localProjects[1].Name = newProjectName
   417  	m.Projects = projects
   418  	if err := fake.WriteRemoteManifest(m); err != nil {
   419  		t.Fatal(err)
   420  	}
   421  	// Check that UpdateUniverse() does not fail.
   422  	if err := fake.UpdateUniverse(true); err != nil {
   423  		t.Fatal(err)
   424  	}
   425  }
   426  
   427  // TestUpdateUniverseRemoteBranch checks that UpdateUniverse can pull from a
   428  // non-master remote branch.
   429  func TestUpdateUniverseRemoteBranch(t *testing.T) {
   430  	localProjects, fake, cleanup := setupUniverse(t)
   431  	defer cleanup()
   432  	s := fake.X.NewSeq()
   433  	if err := fake.UpdateUniverse(false); err != nil {
   434  		t.Fatal(err)
   435  	}
   436  
   437  	// Commit to master branch of a project 1.
   438  	writeReadme(t, fake.X, fake.Projects[localProjects[1].Name], "master commit")
   439  	// Create and checkout a new branch of project 1 and make a new commit.
   440  	git := gitutil.New(s, gitutil.RootDirOpt(fake.Projects[localProjects[1].Name]))
   441  	if err := git.CreateAndCheckoutBranch("non-master"); err != nil {
   442  		t.Fatal(err)
   443  	}
   444  	writeReadme(t, fake.X, fake.Projects[localProjects[1].Name], "non-master commit")
   445  	// Point the manifest to the new non-master branch.
   446  	m, err := fake.ReadRemoteManifest()
   447  	if err != nil {
   448  		t.Fatal(err)
   449  	}
   450  	projects := []project.Project{}
   451  	for _, p := range m.Projects {
   452  		if p.Name == localProjects[1].Name {
   453  			p.RemoteBranch = "non-master"
   454  		}
   455  		projects = append(projects, p)
   456  	}
   457  	m.Projects = projects
   458  	if err := fake.WriteRemoteManifest(m); err != nil {
   459  		t.Fatal(err)
   460  	}
   461  	// Check that UpdateUniverse pulls the commit from the non-master branch.
   462  	if err := fake.UpdateUniverse(false); err != nil {
   463  		t.Fatal(err)
   464  	}
   465  	checkReadme(t, fake.X, localProjects[1], "non-master commit")
   466  }
   467  
   468  func TestFileImportCycle(t *testing.T) {
   469  	jirix, cleanup := jiritest.NewX(t)
   470  	defer cleanup()
   471  
   472  	// Set up the cycle .jiri_manifest -> A -> B -> A
   473  	jiriManifest := project.Manifest{
   474  		LocalImports: []project.LocalImport{
   475  			{File: "A"},
   476  		},
   477  	}
   478  	manifestA := project.Manifest{
   479  		LocalImports: []project.LocalImport{
   480  			{File: "B"},
   481  		},
   482  	}
   483  	manifestB := project.Manifest{
   484  		LocalImports: []project.LocalImport{
   485  			{File: "A"},
   486  		},
   487  	}
   488  	if err := jiriManifest.ToFile(jirix, jirix.JiriManifestFile()); err != nil {
   489  		t.Fatal(err)
   490  	}
   491  	if err := manifestA.ToFile(jirix, filepath.Join(jirix.Root, "A")); err != nil {
   492  		t.Fatal(err)
   493  	}
   494  	if err := manifestB.ToFile(jirix, filepath.Join(jirix.Root, "B")); err != nil {
   495  		t.Fatal(err)
   496  	}
   497  
   498  	// The update should complain about the cycle.
   499  	err := project.UpdateUniverse(jirix, false)
   500  	if got, want := fmt.Sprint(err), "import cycle detected in local manifest files"; !strings.Contains(got, want) {
   501  		t.Errorf("got error %v, want substr %v", got, want)
   502  	}
   503  }
   504  
   505  func TestRemoteImportCycle(t *testing.T) {
   506  	fake, cleanup := jiritest.NewFakeJiriRoot(t)
   507  	defer cleanup()
   508  
   509  	// Set up two remote manifest projects, remote1 and remote1.
   510  	if err := fake.CreateRemoteProject("remote1"); err != nil {
   511  		t.Fatal(err)
   512  	}
   513  	if err := fake.CreateRemoteProject("remote2"); err != nil {
   514  		t.Fatal(err)
   515  	}
   516  	remote1 := fake.Projects["remote1"]
   517  	remote2 := fake.Projects["remote2"]
   518  
   519  	fileA, fileB := filepath.Join(remote1, "A"), filepath.Join(remote2, "B")
   520  
   521  	// Set up the cycle .jiri_manifest -> remote1+A -> remote2+B -> remote1+A
   522  	jiriManifest := project.Manifest{
   523  		Imports: []project.Import{
   524  			{Manifest: "A", Name: "n1", Remote: remote1},
   525  		},
   526  	}
   527  	manifestA := project.Manifest{
   528  		Imports: []project.Import{
   529  			{Manifest: "B", Name: "n2", Remote: remote2},
   530  		},
   531  	}
   532  	manifestB := project.Manifest{
   533  		Imports: []project.Import{
   534  			{Manifest: "A", Name: "n3", Remote: remote1},
   535  		},
   536  	}
   537  	if err := jiriManifest.ToFile(fake.X, fake.X.JiriManifestFile()); err != nil {
   538  		t.Fatal(err)
   539  	}
   540  	if err := manifestA.ToFile(fake.X, fileA); err != nil {
   541  		t.Fatal(err)
   542  	}
   543  	if err := manifestB.ToFile(fake.X, fileB); err != nil {
   544  		t.Fatal(err)
   545  	}
   546  	commitFile(t, fake.X, remote1, fileA, "commit A")
   547  	commitFile(t, fake.X, remote2, fileB, "commit B")
   548  
   549  	// The update should complain about the cycle.
   550  	err := project.UpdateUniverse(fake.X, false)
   551  	if got, want := fmt.Sprint(err), "import cycle detected in remote manifest imports"; !strings.Contains(got, want) {
   552  		t.Errorf("got error %v, want substr %v", got, want)
   553  	}
   554  }
   555  
   556  func TestFileAndRemoteImportCycle(t *testing.T) {
   557  	fake, cleanup := jiritest.NewFakeJiriRoot(t)
   558  	defer cleanup()
   559  
   560  	// Set up two remote manifest projects, remote1 and remote2.
   561  	// Set up two remote manifest projects, remote1 and remote1.
   562  	if err := fake.CreateRemoteProject("remote1"); err != nil {
   563  		t.Fatal(err)
   564  	}
   565  	if err := fake.CreateRemoteProject("remote2"); err != nil {
   566  		t.Fatal(err)
   567  	}
   568  	remote1 := fake.Projects["remote1"]
   569  	remote2 := fake.Projects["remote2"]
   570  	fileA, fileD := filepath.Join(remote1, "A"), filepath.Join(remote1, "D")
   571  	fileB, fileC := filepath.Join(remote2, "B"), filepath.Join(remote2, "C")
   572  
   573  	// Set up the cycle .jiri_manifest -> remote1+A -> remote2+B -> C -> remote1+D -> A
   574  	jiriManifest := project.Manifest{
   575  		Imports: []project.Import{
   576  			{Manifest: "A", Root: "r1", Name: "n1", Remote: remote1},
   577  		},
   578  	}
   579  	manifestA := project.Manifest{
   580  		Imports: []project.Import{
   581  			{Manifest: "B", Root: "r2", Name: "n2", Remote: remote2},
   582  		},
   583  	}
   584  	manifestB := project.Manifest{
   585  		LocalImports: []project.LocalImport{
   586  			{File: "C"},
   587  		},
   588  	}
   589  	manifestC := project.Manifest{
   590  		Imports: []project.Import{
   591  			{Manifest: "D", Root: "r3", Name: "n3", Remote: remote1},
   592  		},
   593  	}
   594  	manifestD := project.Manifest{
   595  		LocalImports: []project.LocalImport{
   596  			{File: "A"},
   597  		},
   598  	}
   599  	if err := jiriManifest.ToFile(fake.X, fake.X.JiriManifestFile()); err != nil {
   600  		t.Fatal(err)
   601  	}
   602  	if err := manifestA.ToFile(fake.X, fileA); err != nil {
   603  		t.Fatal(err)
   604  	}
   605  	if err := manifestB.ToFile(fake.X, fileB); err != nil {
   606  		t.Fatal(err)
   607  	}
   608  	if err := manifestC.ToFile(fake.X, fileC); err != nil {
   609  		t.Fatal(err)
   610  	}
   611  	if err := manifestD.ToFile(fake.X, fileD); err != nil {
   612  		t.Fatal(err)
   613  	}
   614  	commitFile(t, fake.X, remote1, fileA, "commit A")
   615  	commitFile(t, fake.X, remote2, fileB, "commit B")
   616  	commitFile(t, fake.X, remote2, fileC, "commit C")
   617  	commitFile(t, fake.X, remote1, fileD, "commit D")
   618  
   619  	// The update should complain about the cycle.
   620  	err := project.UpdateUniverse(fake.X, false)
   621  	if got, want := fmt.Sprint(err), "import cycle detected"; !strings.Contains(got, want) {
   622  		t.Errorf("got error %v, want substr %v", got, want)
   623  	}
   624  }
   625  
   626  // TestUnsupportedProtocolErr checks that calling
   627  // UnsupportedPrototoclErr.Error() does not result in an infinite loop.
   628  func TestUnsupportedPrototocolErr(t *testing.T) {
   629  	err := project.UnsupportedProtocolErr("foo")
   630  	_ = err.Error()
   631  }
   632  
   633  type binDirTest struct {
   634  	Name        string
   635  	Setup       func(old, new string) error
   636  	Teardown    func(old, new string) error
   637  	Error       string
   638  	CheckBackup bool
   639  }
   640  
   641  func TestTransitionBinDir(t *testing.T) {
   642  	tests := []binDirTest{
   643  		{
   644  			"No old dir",
   645  			func(old, new string) error { return nil },
   646  			nil,
   647  			"",
   648  			false,
   649  		},
   650  		{
   651  			"Empty old dir",
   652  			func(old, new string) error {
   653  				return os.MkdirAll(old, 0777)
   654  			},
   655  			nil,
   656  			"",
   657  			true,
   658  		},
   659  		{
   660  			"Populated old dir",
   661  			func(old, new string) error {
   662  				if err := os.MkdirAll(old, 0777); err != nil {
   663  					return err
   664  				}
   665  				return ioutil.WriteFile(filepath.Join(old, "tool"), []byte("foo"), 0777)
   666  			},
   667  			nil,
   668  			"",
   669  			true,
   670  		},
   671  		{
   672  			"Symlinked old dir",
   673  			func(old, new string) error {
   674  				if err := os.MkdirAll(filepath.Dir(old), 0777); err != nil {
   675  					return err
   676  				}
   677  				return os.Symlink(new, old)
   678  			},
   679  			nil,
   680  			"",
   681  			false,
   682  		},
   683  		{
   684  			"Symlinked old dir pointing elsewhere",
   685  			func(old, new string) error {
   686  				if err := os.MkdirAll(filepath.Dir(old), 0777); err != nil {
   687  					return err
   688  				}
   689  				return os.Symlink(filepath.Dir(new), old)
   690  			},
   691  			nil,
   692  			"",
   693  			true,
   694  		},
   695  		{
   696  			"Unreadable old dir parent",
   697  			func(old, new string) error {
   698  				if err := os.MkdirAll(old, 0777); err != nil {
   699  					return err
   700  				}
   701  				return os.Chmod(filepath.Dir(old), 0222)
   702  			},
   703  			func(old, new string) error {
   704  				return os.Chmod(filepath.Dir(old), 0777)
   705  			},
   706  			"Failed to stat old bin dir",
   707  			false,
   708  		},
   709  		{
   710  			"Unwritable old dir",
   711  			func(old, new string) error {
   712  				if err := os.MkdirAll(old, 0777); err != nil {
   713  					return err
   714  				}
   715  				return os.Chmod(old, 0444)
   716  			},
   717  			func(old, new string) error {
   718  				return os.Chmod(old, 0777)
   719  			},
   720  			"Failed to backup old bin dir",
   721  			false,
   722  		},
   723  		{
   724  			"Unreadable backup dir parent",
   725  			func(old, new string) error {
   726  				if err := os.MkdirAll(old, 0777); err != nil {
   727  					return err
   728  				}
   729  				return os.Chmod(filepath.Dir(new), 0222)
   730  			},
   731  			func(old, new string) error {
   732  				return os.Chmod(filepath.Dir(new), 0777)
   733  			},
   734  			"Failed to stat backup bin dir",
   735  			false,
   736  		},
   737  		{
   738  			"Existing backup dir",
   739  			func(old, new string) error {
   740  				if err := os.MkdirAll(old, 0777); err != nil {
   741  					return err
   742  				}
   743  				return os.MkdirAll(new+".BACKUP", 0777)
   744  			},
   745  			nil,
   746  			"Backup bin dir",
   747  			false,
   748  		},
   749  	}
   750  	for _, test := range tests {
   751  		jirix, cleanup := jiritest.NewX(t)
   752  		if err := testTransitionBinDir(jirix, test); err != nil {
   753  			t.Errorf("%s: %v", test.Name, err)
   754  		}
   755  		cleanup()
   756  	}
   757  }
   758  
   759  func testTransitionBinDir(jirix *jiri.X, test binDirTest) (e error) {
   760  	oldDir, newDir := filepath.Join(jirix.Root, "devtools", "bin"), jirix.BinDir()
   761  	// The new bin dir always exists.
   762  	if err := os.MkdirAll(newDir, 0777); err != nil {
   763  		return fmt.Errorf("make new dir failed: %v", err)
   764  	}
   765  	if err := test.Setup(oldDir, newDir); err != nil {
   766  		return fmt.Errorf("setup failed: %v", err)
   767  	}
   768  	if test.Teardown != nil {
   769  		defer func() {
   770  			if err := test.Teardown(oldDir, newDir); err != nil && e == nil {
   771  				e = fmt.Errorf("teardown failed: %v", err)
   772  			}
   773  		}()
   774  	}
   775  	oldInfo, _ := os.Stat(oldDir)
   776  	switch err := project.TransitionBinDir(jirix); {
   777  	case err != nil && test.Error == "":
   778  		return fmt.Errorf("got error %q, want success", err)
   779  	case err != nil && !strings.Contains(fmt.Sprint(err), test.Error):
   780  		return fmt.Errorf("got error %q, want prefix %q", err, test.Error)
   781  	case err == nil && test.Error != "":
   782  		return fmt.Errorf("got no error, want %q", test.Error)
   783  	case err == nil && test.Error == "":
   784  		// Make sure the symlink exists and is correctly linked.
   785  		link, err := os.Readlink(oldDir)
   786  		if err != nil {
   787  			return fmt.Errorf("old dir isn't a symlink: %v", err)
   788  		}
   789  		if got, want := link, newDir; got != want {
   790  			return fmt.Errorf("old dir symlink got %v, want %v", got, want)
   791  		}
   792  		if test.CheckBackup {
   793  			// Make sure the oldDir was backed up correctly.
   794  			backupDir := filepath.Join(jirix.RootMetaDir(), "bin.BACKUP")
   795  			backupInfo, err := os.Stat(backupDir)
   796  			if err != nil {
   797  				return fmt.Errorf("stat backup dir failed: %v", err)
   798  			}
   799  			if !os.SameFile(oldInfo, backupInfo) {
   800  				return fmt.Errorf("old dir wasn't backed up correctly")
   801  			}
   802  		}
   803  	}
   804  	return nil
   805  }
   806  
   807  func TestManifestToFromBytes(t *testing.T) {
   808  	tests := []struct {
   809  		Manifest project.Manifest
   810  		XML      string
   811  	}{
   812  		{
   813  			project.Manifest{},
   814  			`<manifest>
   815  </manifest>
   816  `,
   817  		},
   818  		{
   819  			project.Manifest{
   820  				Imports: []project.Import{
   821  					{
   822  						Manifest:     "manifest1",
   823  						Name:         "remoteimport1",
   824  						Protocol:     "git",
   825  						Remote:       "remote1",
   826  						RemoteBranch: "master",
   827  					},
   828  					{
   829  						Manifest:     "manifest2",
   830  						Name:         "remoteimport2",
   831  						Protocol:     "git",
   832  						Remote:       "remote2",
   833  						RemoteBranch: "branch2",
   834  					},
   835  				},
   836  				LocalImports: []project.LocalImport{
   837  					{File: "fileimport"},
   838  				},
   839  				Projects: []project.Project{
   840  					{
   841  						Name:         "project1",
   842  						Path:         "path1",
   843  						Protocol:     "git",
   844  						Remote:       "remote1",
   845  						RemoteBranch: "master",
   846  						Revision:     "HEAD",
   847  						GerritHost:   "https://test-review.googlesource.com",
   848  						GitHooks:     "path/to/githooks",
   849  						RunHook:      "path/to/hook",
   850  					},
   851  					{
   852  						Name:         "project2",
   853  						Path:         "path2",
   854  						Protocol:     "git",
   855  						Remote:       "remote2",
   856  						RemoteBranch: "branch2",
   857  						Revision:     "rev2",
   858  					},
   859  				},
   860  				Tools: []project.Tool{
   861  					{
   862  						Data:    "tooldata",
   863  						Name:    "tool",
   864  						Project: "toolproject",
   865  					},
   866  				},
   867  			},
   868  			`<manifest>
   869    <imports>
   870      <import manifest="manifest1" name="remoteimport1" remote="remote1"/>
   871      <import manifest="manifest2" name="remoteimport2" remote="remote2" remotebranch="branch2"/>
   872      <localimport file="fileimport"/>
   873    </imports>
   874    <projects>
   875      <project name="project1" path="path1" remote="remote1" gerrithost="https://test-review.googlesource.com" githooks="path/to/githooks" runhook="path/to/hook"/>
   876      <project name="project2" path="path2" remote="remote2" remotebranch="branch2" revision="rev2"/>
   877    </projects>
   878    <tools>
   879      <tool data="tooldata" name="tool" project="toolproject"/>
   880    </tools>
   881  </manifest>
   882  `,
   883  		},
   884  	}
   885  	for _, test := range tests {
   886  		gotBytes, err := test.Manifest.ToBytes()
   887  		if err != nil {
   888  			t.Errorf("%+v ToBytes failed: %v", test.Manifest, err)
   889  		}
   890  		if got, want := string(gotBytes), test.XML; got != want {
   891  			t.Errorf("%+v ToBytes GOT\n%v\nWANT\n%v", test.Manifest, got, want)
   892  		}
   893  		manifest, err := project.ManifestFromBytes([]byte(test.XML))
   894  		if err != nil {
   895  			t.Errorf("%+v FromBytes failed: %v", test.Manifest, err)
   896  		}
   897  		if got, want := manifest, &test.Manifest; !reflect.DeepEqual(got, want) {
   898  			t.Errorf("%+v FromBytes got %#v, want %#v", test.Manifest, got, want)
   899  		}
   900  	}
   901  }
   902  
   903  func TestProjectToFromFile(t *testing.T) {
   904  	jirix, cleanup := jiritest.NewX(t)
   905  	defer cleanup()
   906  
   907  	tests := []struct {
   908  		Project project.Project
   909  		XML     string
   910  	}{
   911  		{
   912  			// Default fields are dropped when marshaled, and added when unmarshaled.
   913  			project.Project{
   914  				Name:         "project1",
   915  				Path:         filepath.Join(jirix.Root, "path1"),
   916  				Protocol:     "git",
   917  				Remote:       "remote1",
   918  				RemoteBranch: "master",
   919  				Revision:     "HEAD",
   920  			},
   921  			`<project name="project1" path="path1" remote="remote1"/>
   922  `,
   923  		},
   924  		{
   925  			project.Project{
   926  				Name:         "project2",
   927  				Path:         filepath.Join(jirix.Root, "path2"),
   928  				GitHooks:     filepath.Join(jirix.Root, "git-hooks"),
   929  				RunHook:      filepath.Join(jirix.Root, "run-hook"),
   930  				Protocol:     "git",
   931  				Remote:       "remote2",
   932  				RemoteBranch: "branch2",
   933  				Revision:     "rev2",
   934  			},
   935  			`<project name="project2" path="path2" remote="remote2" remotebranch="branch2" revision="rev2" githooks="git-hooks" runhook="run-hook"/>
   936  `,
   937  		},
   938  	}
   939  	for index, test := range tests {
   940  		filename := filepath.Join(jirix.Root, fmt.Sprintf("test-%d", index))
   941  		if err := test.Project.ToFile(jirix, filename); err != nil {
   942  			t.Errorf("%+v ToFile failed: %v", test.Project, err)
   943  		}
   944  		gotBytes, err := jirix.NewSeq().ReadFile(filename)
   945  		if err != nil {
   946  			t.Errorf("%+v ReadFile failed: %v", test.Project, err)
   947  		}
   948  		if got, want := string(gotBytes), test.XML; got != want {
   949  			t.Errorf("%+v ToFile GOT\n%v\nWANT\n%v", test.Project, got, want)
   950  		}
   951  		project, err := project.ProjectFromFile(jirix, filename)
   952  		if err != nil {
   953  			t.Errorf("%+v FromFile failed: %v", test.Project, err)
   954  		}
   955  		if got, want := project, &test.Project; !reflect.DeepEqual(got, want) {
   956  			t.Errorf("%+v FromFile got %#v, want %#v", test.Project, got, want)
   957  		}
   958  	}
   959  }