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 }