go.fuchsia.dev/jiri@v0.0.0-20240502161911-b66513b29486/cmd/jiri/status_test.go (about)

     1  // Copyright 2017 The Fuchsia 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 main
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  	"testing"
    15  
    16  	"go.fuchsia.dev/jiri"
    17  	"go.fuchsia.dev/jiri/gitutil"
    18  	"go.fuchsia.dev/jiri/jiritest"
    19  	"go.fuchsia.dev/jiri/project"
    20  )
    21  
    22  func setDefaultStatusFlags() {
    23  	statusFlags.changes = true
    24  	statusFlags.checkHead = true
    25  	statusFlags.branch = ""
    26  	statusFlags.commits = true
    27  	statusFlags.deleted = false
    28  }
    29  
    30  func createCommits(t *testing.T, fake *jiritest.FakeJiriRoot, localProjects []project.Project) ([]string, []string, []string, []string) {
    31  	cwd, err := os.Getwd()
    32  	if err != nil {
    33  		t.Fatal(err)
    34  	}
    35  	var file2CommitRevs []string
    36  	var file1CommitRevs []string
    37  	var latestCommitRevs []string
    38  	var relativePaths []string
    39  	for i, localProject := range localProjects {
    40  		setDummyUser(t, fake.X, fake.Projects[localProject.Name])
    41  		gitRemote := gitutil.New(fake.X, gitutil.RootDirOpt(fake.Projects[localProject.Name]))
    42  		writeFile(t, fake.X, fake.Projects[localProject.Name], "file1"+strconv.Itoa(i), "file1"+strconv.Itoa(i))
    43  		gitRemote.CreateAndCheckoutBranch("file-1")
    44  		gitRemote.CheckoutBranch("main", localProject.GitSubmodules, false)
    45  		file1CommitRev, _ := gitRemote.CurrentRevision()
    46  		file1CommitRevs = append(file1CommitRevs, file1CommitRev)
    47  		gitRemote.CreateAndCheckoutBranch("file-2")
    48  		gitRemote.CheckoutBranch("main", localProject.GitSubmodules, false)
    49  		writeFile(t, fake.X, fake.Projects[localProject.Name], "file2"+strconv.Itoa(i), "file2"+strconv.Itoa(i))
    50  		file2CommitRev, _ := gitRemote.CurrentRevision()
    51  		file2CommitRevs = append(file2CommitRevs, file2CommitRev)
    52  		writeFile(t, fake.X, fake.Projects[localProject.Name], "file3"+strconv.Itoa(i), "file3"+strconv.Itoa(i))
    53  		file3CommitRev, _ := gitRemote.CurrentRevision()
    54  		latestCommitRevs = append(latestCommitRevs, file3CommitRev)
    55  		relativePath, _ := filepath.Rel(cwd, localProject.Path)
    56  		relativePaths = append(relativePaths, relativePath)
    57  	}
    58  	return file1CommitRevs, file2CommitRevs, latestCommitRevs, relativePaths
    59  }
    60  
    61  func createProjects(t *testing.T, fake *jiritest.FakeJiriRoot, numProjects int) []project.Project {
    62  	localProjects := []project.Project{}
    63  	for i := 0; i < numProjects; i++ {
    64  		name := fmt.Sprintf("project-%d", i)
    65  		path := fmt.Sprintf("path-%d", i)
    66  		if err := fake.CreateRemoteProject(name); err != nil {
    67  			t.Fatal(err)
    68  		}
    69  		p := project.Project{
    70  			Name:   name,
    71  			Path:   filepath.Join(fake.X.Root, path),
    72  			Remote: fake.Projects[name],
    73  		}
    74  		localProjects = append(localProjects, p)
    75  		if err := fake.AddProject(p); err != nil {
    76  			t.Fatal(err)
    77  		}
    78  	}
    79  	return localProjects
    80  }
    81  
    82  func expectedOutput(t *testing.T, fake *jiritest.FakeJiriRoot, localProjects []project.Project,
    83  	latestCommitRevs, currentCommits, changes, currentBranch, relativePaths []string, extraCommitLogs [][]string) string {
    84  	want := ""
    85  	for i, localProject := range localProjects {
    86  		includeForNotHead := statusFlags.checkHead && currentCommits[i] != latestCommitRevs[i]
    87  		includeForChanges := statusFlags.changes && changes[i] != ""
    88  		includeForCommits := statusFlags.commits && extraCommitLogs != nil && len(extraCommitLogs[i]) != 0
    89  		includeProject := (statusFlags.branch == "" && (includeForNotHead || includeForChanges || includeForCommits)) ||
    90  			(statusFlags.branch != "" && statusFlags.branch == currentBranch[i])
    91  		if includeProject {
    92  			gitLocal := gitutil.New(fake.X, gitutil.RootDirOpt(localProject.Path))
    93  			currentLog, err := gitLocal.OneLineLog(currentCommits[i])
    94  			if err != nil {
    95  				t.Error(err)
    96  			}
    97  			want = fmt.Sprintf("%s%s: ", want, relativePaths[i])
    98  			if currentCommits[i] != latestCommitRevs[i] && statusFlags.checkHead {
    99  				log, err := gitLocal.OneLineLog(latestCommitRevs[i])
   100  				if err != nil {
   101  					t.Error(err)
   102  				}
   103  				want = fmt.Sprintf("%s\nJIRI_HEAD: %s", want, log)
   104  				want = fmt.Sprintf("%s\nCurrent Revision: %s", want, currentLog)
   105  			}
   106  			want = fmt.Sprintf("%s\nBranch: ", want)
   107  			branchmsg := currentBranch[i]
   108  			if branchmsg == "" {
   109  				branchmsg = fmt.Sprintf("DETACHED-HEAD(%s)", currentLog)
   110  			}
   111  			want = fmt.Sprintf("%s%s", want, branchmsg)
   112  			if extraCommitLogs != nil && statusFlags.commits && len(extraCommitLogs[i]) != 0 {
   113  				want = fmt.Sprintf("%s\nCommits: %d commit(s) not merged to remote", want, len(extraCommitLogs[i]))
   114  				for _, commitLog := range extraCommitLogs[i] {
   115  					want = fmt.Sprintf("%s\n%s", want, commitLog)
   116  				}
   117  
   118  			}
   119  			if statusFlags.changes && changes[i] != "" {
   120  				want = fmt.Sprintf("%s\n%s", want, changes[i])
   121  			}
   122  			want = fmt.Sprintf("%s\n\n", want)
   123  		}
   124  	}
   125  	want = strings.TrimSpace(want)
   126  	return want
   127  }
   128  
   129  func TestStatus(t *testing.T) {
   130  	setDefaultStatusFlags()
   131  	fake, cleanup := jiritest.NewFakeJiriRoot(t)
   132  	defer cleanup()
   133  
   134  	// Add projects
   135  	numProjects := 3
   136  	localProjects := createProjects(t, fake, numProjects)
   137  	file1CommitRevs, file2CommitRevs, latestCommitRevs, relativePaths := createCommits(t, fake, localProjects)
   138  	if err := fake.UpdateUniverse(false); err != nil {
   139  		t.Fatal(err)
   140  	}
   141  
   142  	for _, lp := range localProjects {
   143  		setDummyUser(t, fake.X, lp.Path)
   144  	}
   145  	// Test no changes
   146  	got := executeStatus(t, fake, "")
   147  	want := ""
   148  	if got != want {
   149  		t.Errorf("got %s, want %s", got, want)
   150  	}
   151  
   152  	// Test when HEAD is on different revsion
   153  	gitLocal := gitutil.New(fake.X, gitutil.RootDirOpt(localProjects[1].Path))
   154  	gitLocal.CheckoutBranch("HEAD~1", localProjects[1].GitSubmodules, false)
   155  	gitLocal = gitutil.New(fake.X, gitutil.RootDirOpt(localProjects[2].Path))
   156  	gitLocal.CheckoutBranch("file-2", localProjects[2].GitSubmodules, false)
   157  	got = executeStatus(t, fake, "")
   158  	currentCommits := []string{latestCommitRevs[0], file2CommitRevs[1], file1CommitRevs[2]}
   159  	currentBranch := []string{"", "", "file-2"}
   160  	changes := []string{"", "", ""}
   161  	want = expectedOutput(t, fake, localProjects, latestCommitRevs, currentCommits, changes, currentBranch, relativePaths, nil)
   162  	if !equal(got, want) {
   163  		t.Errorf("got %s, want %s", got, want)
   164  	}
   165  
   166  	// Test combinations of tracked and untracked changes
   167  	newfile(t, localProjects[0].Path, "untracked1")
   168  	newfile(t, localProjects[0].Path, "untracked2")
   169  	newfile(t, localProjects[2].Path, "uncommitted.go")
   170  	if err := gitLocal.Add("uncommitted.go"); err != nil {
   171  		t.Error(err)
   172  	}
   173  	got = executeStatus(t, fake, "")
   174  	currentCommits = []string{latestCommitRevs[0], file2CommitRevs[1], file1CommitRevs[2]}
   175  	currentBranch = []string{"", "", "file-2"}
   176  	changes = []string{"?? untracked1\n?? untracked2", "", "A  uncommitted.go"}
   177  	want = expectedOutput(t, fake, localProjects, latestCommitRevs, currentCommits, changes, currentBranch, relativePaths, nil)
   178  	if !equal(got, want) {
   179  		t.Errorf("got %s, want %s", got, want)
   180  	}
   181  }
   182  
   183  func TestStatusWhenUserUpdatesGitTree(t *testing.T) {
   184  	setDefaultStatusFlags()
   185  	fake, cleanup := jiritest.NewFakeJiriRoot(t)
   186  	defer cleanup()
   187  
   188  	// Add projects
   189  	numProjects := 1
   190  	localProjects := createProjects(t, fake, numProjects)
   191  	if err := fake.UpdateUniverse(false); err != nil {
   192  		t.Fatal(err)
   193  	}
   194  
   195  	// write to remote
   196  	writeFile(t, fake.X, fake.Projects[localProjects[0].Name], "file", "file")
   197  	// git fetch
   198  	gitLocal := gitutil.New(fake.X, gitutil.RootDirOpt(localProjects[0].Path))
   199  	if err := gitLocal.Fetch("origin", false); err != nil {
   200  		t.Fatal(err)
   201  	}
   202  
   203  	got := executeStatus(t, fake, "")
   204  	want := "" // no change
   205  	if got != want {
   206  		t.Errorf("got %s, want %s", got, want)
   207  	}
   208  }
   209  
   210  func TestStatusDeleted(t *testing.T) {
   211  	setDefaultStatusFlags()
   212  	fake, cleanup := jiritest.NewFakeJiriRoot(t)
   213  	defer cleanup()
   214  
   215  	// Add projects
   216  	numProjects := 5
   217  	createProjects(t, fake, numProjects)
   218  	if err := fake.UpdateUniverse(false); err != nil {
   219  		t.Fatal(err)
   220  	}
   221  
   222  	// delete some projects
   223  	manifest, err := fake.ReadRemoteManifest()
   224  	if err != nil {
   225  		t.Fatal(err)
   226  	}
   227  	deletedProjs := manifest.Projects[3:]
   228  	manifest.Projects = manifest.Projects[0:3]
   229  	if err := fake.WriteRemoteManifest(manifest); err != nil {
   230  		t.Fatal(err)
   231  	}
   232  	if err := fake.UpdateUniverse(false); err != nil {
   233  		t.Fatal(err)
   234  	}
   235  
   236  	statusFlags.deleted = true
   237  
   238  	got := executeStatus(t, fake, "")
   239  	numOfLines := len(strings.Split(got, "\n"))
   240  	if numOfLines != 3 {
   241  		t.Errorf("got %s, wanted 3 deleted projects", got)
   242  	}
   243  	for _, dp := range deletedProjs {
   244  		if !strings.Contains(got, dp.Name) {
   245  			t.Fatalf("project %s should have been deleted, got\n%s", dp.Name, got)
   246  		}
   247  		if !strings.Contains(got, dp.Path) {
   248  			t.Fatalf("project %s should have been deleted, got\n%s", dp.Path, got)
   249  		}
   250  	}
   251  }
   252  
   253  func statusFlagsTest(t *testing.T) {
   254  	fake, cleanup := jiritest.NewFakeJiriRoot(t)
   255  	defer cleanup()
   256  
   257  	// Add projects
   258  	numProjects := 6
   259  	localProjects := createProjects(t, fake, numProjects)
   260  	file1CommitRevs, file2CommitRevs, latestCommitRevs, relativePaths := createCommits(t, fake, localProjects)
   261  	if err := fake.UpdateUniverse(false); err != nil {
   262  		t.Fatal(err)
   263  	}
   264  	gitLocals := make([]*gitutil.Git, numProjects)
   265  	for i, localProject := range localProjects {
   266  		gitLocal := gitutil.New(fake.X, gitutil.UserNameOpt("John Doe"), gitutil.UserEmailOpt("john.doe@example.com"), gitutil.RootDirOpt(localProject.Path))
   267  		gitLocals[i] = gitLocal
   268  	}
   269  
   270  	gitLocals[0].CheckoutBranch("HEAD~1", localProjects[0].GitSubmodules, false)
   271  	gitLocals[1].CheckoutBranch("file-2", localProjects[1].GitSubmodules, false)
   272  	gitLocals[3].CheckoutBranch("HEAD~2", localProjects[3].GitSubmodules, false)
   273  	gitLocals[4].CheckoutBranch("main", localProjects[4].GitSubmodules, false)
   274  	gitLocals[5].CheckoutBranch("main", localProjects[5].GitSubmodules, false)
   275  
   276  	newfile(t, localProjects[0].Path, "untracked1")
   277  	newfile(t, localProjects[0].Path, "untracked2")
   278  
   279  	newfile(t, localProjects[1].Path, "uncommitted.go")
   280  	if err := gitLocals[1].Add("uncommitted.go"); err != nil {
   281  		t.Error(err)
   282  	}
   283  
   284  	newfile(t, localProjects[2].Path, "untracked1")
   285  	newfile(t, localProjects[2].Path, "uncommitted.go")
   286  	if err := gitLocals[2].Add("uncommitted.go"); err != nil {
   287  		t.Error(err)
   288  	}
   289  
   290  	extraCommits5 := []string{}
   291  	for i := 0; i < 2; i++ {
   292  		file := fmt.Sprintf("extrafile%d", i)
   293  		writeFile(t, fake.X, localProjects[5].Path, file, file+"log")
   294  		log, err := gitLocals[5].OneLineLog("HEAD")
   295  		if err != nil {
   296  			t.Error(err)
   297  		}
   298  		extraCommits5 = append([]string{log}, extraCommits5...)
   299  	}
   300  	gl5 := gitutil.New(fake.X, gitutil.RootDirOpt(localProjects[5].Path))
   301  	currentCommit5, err := gl5.CurrentRevision()
   302  	if err != nil {
   303  		t.Error(err)
   304  	}
   305  	got := executeStatus(t, fake, "")
   306  	currentCommits := []string{file2CommitRevs[0], file1CommitRevs[1], latestCommitRevs[2], file1CommitRevs[3], latestCommitRevs[4], currentCommit5}
   307  	extraCommitLogs := [][]string{nil, nil, nil, nil, nil, extraCommits5}
   308  	currentBranch := []string{"", "file-2", "", "", "main", "main"}
   309  	changes := []string{"?? untracked1\n?? untracked2", "A  uncommitted.go", "A  uncommitted.go\n?? untracked1", "", "", ""}
   310  	want := expectedOutput(t, fake, localProjects, latestCommitRevs, currentCommits, changes, currentBranch, relativePaths, extraCommitLogs)
   311  	if !equal(got, want) {
   312  		printStatusFlags()
   313  		t.Errorf("got %s, want %s", got, want)
   314  	}
   315  }
   316  
   317  func printStatusFlags() {
   318  	fmt.Printf("changes=%t, check-head=%t, commits=%t\n", statusFlags.changes, statusFlags.checkHead, statusFlags.commits)
   319  }
   320  
   321  func TestStatusFlags(t *testing.T) {
   322  	setDefaultStatusFlags()
   323  	statusFlagsTest(t)
   324  
   325  	setDefaultStatusFlags()
   326  	statusFlags.changes = false
   327  	statusFlagsTest(t)
   328  
   329  	setDefaultStatusFlags()
   330  	statusFlags.changes = false
   331  	statusFlags.checkHead = false
   332  	statusFlagsTest(t)
   333  
   334  	setDefaultStatusFlags()
   335  	statusFlags.checkHead = false
   336  	statusFlagsTest(t)
   337  
   338  	setDefaultStatusFlags()
   339  	statusFlags.changes = false
   340  	statusFlags.checkHead = false
   341  	statusFlags.branch = "main"
   342  	statusFlagsTest(t)
   343  
   344  	setDefaultStatusFlags()
   345  	statusFlags.checkHead = false
   346  	statusFlags.branch = "main"
   347  	statusFlags.commits = false
   348  	statusFlagsTest(t)
   349  
   350  	setDefaultStatusFlags()
   351  	statusFlags.changes = false
   352  	statusFlags.branch = "main"
   353  	statusFlagsTest(t)
   354  
   355  	setDefaultStatusFlags()
   356  	statusFlags.changes = false
   357  	statusFlags.checkHead = false
   358  	statusFlags.branch = "file-2"
   359  	statusFlagsTest(t)
   360  
   361  	setDefaultStatusFlags()
   362  	statusFlags.checkHead = false
   363  	statusFlags.branch = "file-2"
   364  	statusFlagsTest(t)
   365  
   366  	setDefaultStatusFlags()
   367  	statusFlags.changes = false
   368  	statusFlags.branch = "file-2"
   369  	statusFlagsTest(t)
   370  }
   371  
   372  func equal(first, second string) bool {
   373  	firstStrings := strings.Split(first, "\n\n")
   374  	secondStrings := strings.Split(second, "\n\n")
   375  	if len(firstStrings) != len(secondStrings) {
   376  		return false
   377  	}
   378  	sort.Strings(firstStrings)
   379  	sort.Strings(secondStrings)
   380  	for i, first := range firstStrings {
   381  		if first != secondStrings[i] {
   382  			return false
   383  		}
   384  	}
   385  	return true
   386  }
   387  
   388  func executeStatus(t *testing.T, fake *jiritest.FakeJiriRoot, args ...string) string {
   389  	stderr := ""
   390  	runCmd := func() {
   391  		if err := runStatus(fake.X, args); err != nil {
   392  			stderr = err.Error()
   393  		}
   394  	}
   395  	stdout, _, err := runfunc(runCmd)
   396  	if err != nil {
   397  		t.Fatal(err)
   398  	}
   399  	return strings.TrimSpace(strings.Join([]string{stdout, stderr}, " "))
   400  }
   401  
   402  func writeFile(t *testing.T, jirix *jiri.X, projectDir, fileName, message string) {
   403  	path, perm := filepath.Join(projectDir, fileName), os.FileMode(0644)
   404  	if err := os.WriteFile(path, []byte(message), perm); err != nil {
   405  		t.Fatalf("WriteFile(%s, %d) failed: %s", path, perm, err)
   406  	}
   407  	if err := gitutil.New(jirix, gitutil.RootDirOpt(projectDir),
   408  		gitutil.UserNameOpt("John Doe"),
   409  		gitutil.UserEmailOpt("john.doe@example.com")).CommitFile(path,
   410  		message); err != nil {
   411  		t.Fatal(err)
   412  	}
   413  }
   414  
   415  func setDummyUser(t *testing.T, jirix *jiri.X, projectDir string) {
   416  	git := gitutil.New(jirix, gitutil.RootDirOpt(projectDir))
   417  	if err := git.Config("user.email", "john.doe@example.com"); err != nil {
   418  		t.Fatal(err)
   419  	}
   420  	if err := git.Config("user.name", "John Doe"); err != nil {
   421  		t.Fatal(err)
   422  	}
   423  }
   424  
   425  func newfile(t *testing.T, dir, file string) {
   426  	testfile := filepath.Join(dir, file)
   427  	_, err := os.Create(testfile)
   428  	if err != nil {
   429  		t.Errorf("failed to create %s: %s", testfile, err)
   430  	}
   431  }