go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/submodule_update/submodule/submodule_test.go (about) 1 // Copyright 2024 The Fuchsia Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package submodule 6 7 import ( 8 "fmt" 9 "os" 10 "os/exec" 11 "path" 12 "path/filepath" 13 "regexp" 14 "strconv" 15 "strings" 16 "testing" 17 18 "github.com/google/go-cmp/cmp" 19 "go.fuchsia.dev/infra/cmd/submodule_update/gitutil" 20 ) 21 22 // createTestdataTemp copies the testdata to a temp folder compatible with git. 23 func createTestdataTemp(t *testing.T) (string, error) { 24 t.Helper() 25 uniqTempDir := t.TempDir() 26 fromPath := "../testdata" 27 if err := exec.Command("cp", "-R", fromPath, uniqTempDir).Run(); err != nil { 28 t.Fatalf("Error copying from %s to %s (%q)", fromPath, uniqTempDir, err) 29 return "", err 30 } 31 return uniqTempDir, nil 32 } 33 34 // createSuperproject clones the testdata superproject to a temp directory. 35 func createSuperproject(t *testing.T, recurseSubmodules bool) (g *gitutil.Git, err error) { 36 t.Helper() 37 superprojectRemoteTemp, err := createTestdataTemp(t) 38 if err != nil { 39 t.Fatalf("Failed to create superproject remote temp dir: %s", err) 40 return nil, err 41 } 42 43 superprojectRoot := t.TempDir() 44 scm := gitutil.New(gitutil.RootDirOpt(superprojectRoot)) 45 repo := filepath.Join(superprojectRemoteTemp, "testdata", "remote", "super") 46 if err := scm.Clone(repo, superprojectRoot, gitutil.RecurseSubmodulesOpt(recurseSubmodules)); err != nil { 47 t.Fatalf("Failed to clone superproject: %s", err) 48 return nil, err 49 } 50 51 gitConfig := map[string]string{ 52 // Allow cloning local repos, since this is required for tests that use 53 // this mock data. 54 "protocol.file.allow": "always", 55 } 56 t.Setenv("GIT_CONFIG_COUNT", strconv.Itoa(len(gitConfig))) 57 i := 0 58 for k, v := range gitConfig { 59 t.Setenv(fmt.Sprintf("GIT_CONFIG_KEY_%d", i), k) 60 t.Setenv(fmt.Sprintf("GIT_CONFIG_VALUE_%d", i), v) 61 i++ 62 } 63 64 return scm, nil 65 } 66 67 func createSubmodules(t *testing.T, submodules map[string]string) Submodules { 68 t.Helper() 69 var subModules = Submodules{} 70 for subPath, rev := range submodules { 71 subM := Submodule{ 72 Name: subPath, 73 Path: subPath, 74 Revision: rev, 75 // For testing, fake the remote 76 Remote: filepath.Join("remote", subPath), 77 } 78 subModules[subM.Key()] = subM 79 } 80 return subModules 81 } 82 83 func getSubmodules(t *testing.T, g *gitutil.Git, cached bool) Submodules { 84 t.Helper() 85 gitSubmodules, err := gitSubmodules(g, cached) 86 if err != nil { 87 t.Fatalf("Git submodules creation failed: %s", err) 88 } 89 return gitSubmodules 90 } 91 92 func TestSubmoduleDiff(t *testing.T) { 93 initialSub := createSubmodules(t, map[string]string{"sub1": "12345a", "sub2": "12345b"}) 94 // Change remote for "sub1" to "sub1_new_remote" 95 remoteChangeSub := createSubmodules(t, map[string]string{"sub1": "12345a", "sub2": "12345b"}) 96 remoteSub1 := remoteChangeSub[Key("sub1")] 97 remoteSub1.Remote = path.Join("new", "remote", "sub1") 98 remoteChangeSub[Key("sub1")] = remoteSub1 99 100 type diffTest struct { 101 name string 102 gitSubmodules Submodules 103 jiriSubmodules Submodules 104 want Diff 105 } 106 testCases := []diffTest{ 107 { 108 name: "no diff", 109 gitSubmodules: initialSub, 110 jiriSubmodules: initialSub, 111 want: Diff{}, 112 }, 113 { 114 name: "add sub3", 115 gitSubmodules: initialSub, 116 jiriSubmodules: createSubmodules(t, map[string]string{ 117 "sub1": "12345a", 118 "sub2": "12345b", 119 "sub3": "12345c", 120 }), 121 want: Diff{ 122 NewSubmodules: []DiffSubmodule{{ 123 Name: "sub3", 124 Path: "sub3", 125 Revision: "12345c", 126 Remote: path.Join("remote", "sub3"), 127 }}, 128 }, 129 }, 130 { 131 name: "delete sub1", 132 gitSubmodules: initialSub, 133 jiriSubmodules: createSubmodules(t, map[string]string{ 134 "sub2": "12345b", 135 }), 136 want: Diff{ 137 DeletedSubmodules: []DiffSubmodule{{ 138 Path: "sub1", 139 Revision: "12345a", 140 Remote: path.Join("remote", "sub1")}}, 141 }, 142 }, 143 { 144 name: "don't delete nameless submodule", 145 gitSubmodules: func() Submodules { 146 submodules := createSubmodules(t, map[string]string{ 147 "sub1": "12345a", 148 "nameless": "12345b", 149 }) 150 subm := submodules[Key("nameless")] 151 subm.Name = "" 152 submodules[Key("nameless")] = subm 153 return submodules 154 }(), 155 jiriSubmodules: createSubmodules(t, map[string]string{ 156 "sub1": "12345a", 157 }), 158 want: Diff{}, 159 }, 160 { 161 name: "update sub1 (new revision)", 162 gitSubmodules: initialSub, 163 jiriSubmodules: createSubmodules(t, map[string]string{ 164 "sub1": "12346a", 165 "sub2": "12345b", 166 }), 167 want: Diff{ 168 UpdatedSubmodules: []DiffSubmodule{{ 169 Name: "sub1", 170 Path: "sub1", 171 Revision: "12346a", 172 OldRevision: "12345a", 173 }}, 174 }, 175 }, 176 { 177 // Processed as a delete (old path) and a new submodule (new path). 178 name: "move sub1 (change path)", 179 gitSubmodules: initialSub, 180 jiriSubmodules: createSubmodules(t, map[string]string{ 181 "sub3": "12345a", 182 "sub2": "12345b", 183 }), 184 want: Diff{ 185 NewSubmodules: []DiffSubmodule{{ 186 Name: "sub3", 187 Path: "sub3", 188 Revision: "12345a", 189 Remote: path.Join("remote", "sub3")}}, 190 DeletedSubmodules: []DiffSubmodule{{ 191 Path: "sub1", 192 Revision: "12345a", 193 Remote: path.Join("remote", "sub1")}}, 194 }, 195 }, 196 { 197 // Processed as a delete (old remote) and a new submodule (new remote) 198 name: "move sub1 (change remote)", 199 gitSubmodules: initialSub, 200 jiriSubmodules: remoteChangeSub, 201 want: Diff{ 202 NewSubmodules: []DiffSubmodule{{ 203 Name: "sub1", 204 Path: "sub1", 205 Revision: "12345a", 206 Remote: path.Join("new", "remote", "sub1")}}, 207 DeletedSubmodules: []DiffSubmodule{{ 208 Path: "sub1", 209 Revision: "12345a", 210 Remote: path.Join("remote", "sub1")}}, 211 }, 212 }, 213 } 214 215 for _, tc := range testCases { 216 t.Run(tc.name, func(t *testing.T) { 217 got, err := getDiff(tc.gitSubmodules, tc.jiriSubmodules) 218 if err != nil { 219 t.Fatalf("GetDiff failed with: %s", err) 220 } 221 222 if diff := cmp.Diff(tc.want, got); diff != "" { 223 t.Errorf("GetDiff failed (-want +got):\n%s", diff) 224 } 225 }) 226 } 227 } 228 229 func TestUpdateCommitMessage(t *testing.T) { 230 type msgTest struct { 231 message string 232 want string 233 } 234 testCases := []msgTest{ 235 // No [roll] 236 { 237 message: "Hello\nworld", 238 want: "Hello\nworld", 239 }, 240 // [roll] not at start 241 { 242 message: "Hello\nworld [roll] ", 243 want: "Hello\nworld [roll] ", 244 }, 245 // [roll] at start 246 { 247 message: "[roll] Hello\nworld", 248 want: "[superproject] Hello\nworld", 249 }, 250 } 251 for _, tc := range testCases { 252 got := updateCommitMessage(tc.message) 253 if diff := cmp.Diff(tc.want, got); diff != "" { 254 t.Errorf("updateCommitMessage failed (-want +got):\n%s", diff) 255 } 256 } 257 } 258 259 var gitSHA1Re = regexp.MustCompile(`^[0-9a-f]{40}$`) 260 261 func TestSubmoduleStatusParsing(t *testing.T) { 262 gitSuperproject, err := createSuperproject(t, false) 263 if err != nil { 264 t.Fatalf("Git superproject creation failed: %s", err) 265 } 266 267 gitSubmodules := getSubmodules(t, gitSuperproject, true) 268 269 expected := []Submodule{ 270 { 271 Name: "sub1", 272 Revision: "rev1", 273 Path: "sub1", 274 Remote: "../sub1/", 275 }, 276 { 277 Name: "sub2", 278 Revision: "rev2", 279 Path: "sub2", 280 Remote: "../sub2/", 281 }, 282 } 283 for _, subM := range expected { 284 if submodule, ok := gitSubmodules[Key(subM.Path)]; ok { 285 if subM.Remote != submodule.Remote { 286 t.Errorf("Remote (%s) didn't match expected %s", submodule.Remote, subM.Remote) 287 } 288 if !gitSHA1Re.MatchString(submodule.Revision) { 289 t.Errorf("Revision (%s) didn't match SHA1 regex", submodule.Revision) 290 } 291 } else { 292 t.Errorf("Expected submodule %s missing", subM.Path) 293 } 294 } 295 } 296 297 func TestJiriProjectParsing(t *testing.T) { 298 jiriProjectsPath := filepath.Join("..", "testdata", "jiri_projects_public.json") 299 300 jiriSubmodules, err := jiriProjectsToSubmodule(jiriProjectsPath) 301 if err != nil { 302 t.Fatalf("Jiri project parsing failed: %s", err) 303 } 304 expectedPaths := []string{"sub1", "sub2"} 305 for _, path := range expectedPaths { 306 if submodule, ok := jiriSubmodules[Key(path)]; ok { 307 if !gitSHA1Re.MatchString(submodule.Revision) { 308 t.Errorf("Revision (%s) didn't match SHA1 regex", submodule.Revision) 309 } 310 if submodule.Remote == "" { 311 t.Errorf("Expected remote for submodule %s not present", submodule.Path) 312 } 313 } else { 314 t.Errorf("Expected submodule %s missing", path) 315 } 316 } 317 } 318 319 func TestUpdateSubmodules(t *testing.T) { 320 subPath := "sub1" 321 wantRev := "4c473777b21946176bc7d157d195a29c108da6c6" 322 gitSuperproject, err := createSuperproject(t, false) 323 if err != nil { 324 t.Fatalf("Failed to create superproject: %s", err) 325 } 326 diff := []DiffSubmodule{{ 327 Path: subPath, 328 Revision: wantRev, 329 }} 330 if err := updateSubmodules(gitSuperproject, diff, gitSuperproject.RootDir()); err != nil { 331 cmd := exec.Command("tree", gitSuperproject.RootDir()) 332 cmd.Stdout = os.Stdout 333 if e := cmd.Run(); e != nil { 334 t.Fatal(e) 335 } 336 t.Fatalf("Failed to update submodules: %s", err) 337 } 338 gitSubmodules := getSubmodules(t, gitSuperproject, false) 339 gotRev := gitSubmodules[Key(subPath)].Revision 340 if gotRev != wantRev { 341 t.Errorf("Expected %s for submodule revision, got %s", wantRev, gotRev) 342 } 343 } 344 345 func TestUpdateSubmodulesEmpty(t *testing.T) { 346 gitSuperproject, err := createSuperproject(t, false) 347 if err != nil { 348 t.Fatalf("Failed to create superproject: %s", err) 349 } 350 want := getSubmodules(t, gitSuperproject, false) 351 diff := Diff{} 352 if err := updateSubmodules(gitSuperproject, diff.UpdatedSubmodules, "tmp/superproject"); err != nil { 353 t.Fatalf("Failed to update submodules: %s", err) 354 } 355 got := getSubmodules(t, gitSuperproject, false) 356 if diff := cmp.Diff(want, got); diff != "" { 357 t.Errorf("Submodule status changed (-want +got):\n%s", diff) 358 } 359 } 360 361 func TestDeleteSubmodules(t *testing.T) { 362 subPath := "sub1" 363 gitSuperproject, err := createSuperproject(t, false) 364 if err != nil { 365 t.Fatalf("Failed to create superproject: %s", err) 366 } 367 diff := []DiffSubmodule{{ 368 Path: subPath, 369 }} 370 if err := deleteSubmodules(gitSuperproject, diff); err != nil { 371 t.Fatalf("Failed to delete submodules: %s", err) 372 } 373 gitSubmodules := getSubmodules(t, gitSuperproject, false) 374 if _, ok := gitSubmodules[Key(subPath)]; ok { 375 t.Errorf("Submodule %s still present, expected it to be removed", subPath) 376 } 377 } 378 379 func TestDeleteSubmodulesEmpty(t *testing.T) { 380 gitSuperproject, err := createSuperproject(t, false) 381 if err != nil { 382 t.Fatalf("Failed to create superproject: %s", err) 383 } 384 want := getSubmodules(t, gitSuperproject, false) 385 diff := Diff{} 386 if err := deleteSubmodules(gitSuperproject, diff.DeletedSubmodules); err != nil { 387 t.Fatalf("Failed to delete submodules: %s", err) 388 } 389 got := getSubmodules(t, gitSuperproject, false) 390 if diff := cmp.Diff(want, got); diff != "" { 391 t.Errorf("Submodule status changed (-want +got):\n%s", diff) 392 } 393 } 394 395 func TestAddSubmodules(t *testing.T) { 396 subPath := "sub3" 397 gitSuperproject, err := createSuperproject(t, false) 398 if err != nil { 399 t.Fatalf("Failed to create superproject: %s", err) 400 } 401 testdataTemp, err := createTestdataTemp(t) 402 if err != nil { 403 t.Fatalf("Failed to create temp testdata dir: %s", err) 404 } 405 remotePath := filepath.Join(testdataTemp, "testdata", "remote", "sub3") 406 diff := []DiffSubmodule{{ 407 Path: subPath, 408 Remote: remotePath, 409 Revision: "b6c817cd8a01b209f3b4f89cb55816f4173bbf4e", 410 }} 411 if err := addSubmodules(gitSuperproject, diff); err != nil { 412 t.Fatalf("Failed to add submodules: %s", err) 413 } 414 gitSubmodules := getSubmodules(t, gitSuperproject, false) 415 if _, ok := gitSubmodules[Key(subPath)]; !ok { 416 t.Errorf("Submodule %s not present, expected it to be added", subPath) 417 } 418 } 419 420 func TestAddSubmodulesEmpty(t *testing.T) { 421 gitSuperproject, err := createSuperproject(t, false) 422 if err != nil { 423 t.Fatalf("Failed to create superproject: %s", err) 424 } 425 want := getSubmodules(t, gitSuperproject, false) 426 diff := Diff{} 427 if err := addSubmodules(gitSuperproject, diff.NewSubmodules); err != nil { 428 t.Fatalf("Failed to add submodules: %s", err) 429 } 430 got := getSubmodules(t, gitSuperproject, false) 431 if diff := cmp.Diff(want, got); diff != "" { 432 t.Errorf("Submodule status changed (-want +got):\n%s", diff) 433 } 434 } 435 436 func TestCopyFile(t *testing.T) { 437 srcPath := "../testdata/ensure/cipd.ensure" 438 tempDir := t.TempDir() 439 dstPath := filepath.Join(tempDir, "copytest") 440 441 // Test new file creation 442 if err := copyFile(srcPath, dstPath); err != nil { 443 t.Fatalf("Failed to copy file: %s", err) 444 } 445 srcData, err := os.ReadFile(srcPath) 446 if err != nil { 447 t.Fatalf("Failed to read source file: %s", err) 448 } 449 dstData, err := os.ReadFile(dstPath) 450 if err != nil { 451 t.Fatalf("Failed to read destination file: %s", err) 452 } 453 if diff := cmp.Diff(string(srcData), string(dstData)); diff != "" { 454 t.Errorf("CopyFile to new file failed (-want +got):\n%s", diff) 455 } 456 457 // Test truncate/overwrite existing file 458 srcPath = "../testdata/ensure/cipd_internal.ensure" 459 if err = copyFile(srcPath, dstPath); err != nil { 460 t.Fatalf("Failed to copy file: %s", err) 461 } 462 srcData, err = os.ReadFile(srcPath) 463 if err != nil { 464 t.Fatalf("Failed to read source file: %s", err) 465 } 466 dstData, err = os.ReadFile(dstPath) 467 if err != nil { 468 t.Fatalf("Failed to read destination file: %s", err) 469 } 470 if diff := cmp.Diff(string(srcData), string(dstData)); diff != "" { 471 t.Errorf("CopyFile overwrite file failed (-want +got):\n%s", diff) 472 } 473 } 474 475 func TestCopyCIPDEnsure(t *testing.T) { 476 ensurePath := "../testdata/ensure/" 477 cipdPaths := map[string]string{ 478 "ensure": filepath.Join(ensurePath, "cipd.ensure"), 479 "internalEnsure": filepath.Join(ensurePath, "cipd_internal.ensure"), 480 "internalVersion": filepath.Join(ensurePath, "cipd_internal.version"), 481 "version": filepath.Join(ensurePath, "cipd.version"), 482 } 483 gitSuperproject, err := createSuperproject(t, false) 484 if err != nil { 485 t.Fatalf("Failed to create superproject: %s", err) 486 } 487 cipdPath := filepath.Join(gitSuperproject.RootDir(), "tools", "superproject") 488 err = copyCIPDEnsureToSuperproject(cipdPaths, cipdPath) 489 if err != nil { 490 t.Fatalf("Failed to copy ensure files: %s", err) 491 } 492 ensureFileList := []string{filepath.Join(cipdPath, path.Base(cipdPaths["ensure"])), filepath.Join(cipdPath, path.Base(cipdPaths["internalEnsure"]))} 493 versionFileList := []string{filepath.Join(cipdPath, path.Base(cipdPaths["version"])), filepath.Join(cipdPath, path.Base(cipdPaths["internalVersion"]))} 494 for _, dstPath := range ensureFileList { 495 dstDataBytes, err := os.ReadFile(dstPath) 496 if err != nil { 497 t.Fatalf("Failed to read destination file: %s", err) 498 } 499 dstData := string(dstDataBytes) 500 if !strings.HasSuffix(dstData, "Ensure updated\n") { 501 t.Fatalf("Ensure file not updated.") 502 } 503 } 504 for _, dstPath := range versionFileList { 505 dstDataBytes, err := os.ReadFile(dstPath) 506 if err != nil { 507 t.Fatalf("Failed to read destination file: %s", err) 508 } 509 dstData := string(dstDataBytes) 510 if !strings.HasSuffix(dstData, "Version updated\n") { 511 t.Fatalf("Version file not updated.") 512 } 513 } 514 515 } 516 517 // TODO(olivernewman): Re-enable these tests (or find another way to provide test-coverage) 518 // This would require generating a jiri projects input file to match the remotes to the superproject temp directory 519 // because remotes are considered in determining new/deleted/updated submodules. 520 521 /* 522 func TestUpdateSuperprojectRunsNoRecurse(t *testing.T) { 523 gitSuperproject, err := createSuperproject(t, false) 524 if err != nil { 525 t.Fatalf("Failed to create superproject: %s", err) 526 } 527 jiriProjectsPath := "../testdata/jiri_projects_public.json" 528 outputPath := t.TempDir() 529 outputJSONPath := filepath.Join(outputPath, "json_output.json") 530 message := "Commit Message" 531 UpdateSuperproject(gitSuperproject, message, jiriProjectsPath, outputJSONPath) 532 } 533 534 func TestUpdateSuperprojectRunsWithRecurse(t *testing.T) { 535 gitSuperproject, err := createSuperproject(t, true) 536 if err != nil { 537 t.Fatalf("Failed to create superproject: %s", err) 538 } 539 jiriProjectsPath := "../testdata/jiri_projects_public.json" 540 outputPath := t.TempDir() 541 outputJSONPath := filepath.Join(outputPath, "json_output.json") 542 message := "Commit Message" 543 UpdateSuperproject(gitSuperproject, message, jiriProjectsPath, outputJSONPath) 544 } 545 */