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  */