github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/filemetadata/filemetadata_test.go (about)

     1  package filemetadata
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/pkg/xattr"
    11  
    12  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/digest"
    13  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/testutil"
    14  	"github.com/google/go-cmp/cmp"
    15  	"github.com/google/go-cmp/cmp/cmpopts"
    16  )
    17  
    18  const (
    19  	mockedHash = "000000000000000000000000000000000000000000000000000000000000000a"
    20  	targetFile = "test.txt"
    21  )
    22  
    23  var (
    24  	ignoreMtime = cmpopts.IgnoreFields(Metadata{}, "MTime")
    25  )
    26  
    27  func TestComputeFilesNoXattr(t *testing.T) {
    28  	tests := []struct {
    29  		name       string
    30  		contents   string
    31  		executable bool
    32  	}{
    33  		{
    34  			name:     "empty",
    35  			contents: "",
    36  		},
    37  		{
    38  			name:     "non-executable",
    39  			contents: "bla",
    40  		},
    41  		{
    42  			name:       "executable",
    43  			contents:   "foo",
    44  			executable: true,
    45  		},
    46  	}
    47  	for _, tc := range tests {
    48  		t.Run(tc.name, func(t *testing.T) {
    49  			before := time.Now().Truncate(time.Second)
    50  			time.Sleep(5 * time.Second)
    51  			filename, err := testutil.CreateFile(t, tc.executable, tc.contents)
    52  			if err != nil {
    53  				t.Fatalf("Failed to create tmp file for testing digests: %v", err)
    54  			}
    55  			after := time.Now().Truncate(time.Second).Add(time.Second)
    56  			t.Cleanup(func() { os.RemoveAll(filename) })
    57  			got := Compute(filename)
    58  			if got.Err != nil {
    59  				t.Errorf("Compute(%v) failed. Got error: %v", filename, got.Err)
    60  			}
    61  			want := &Metadata{
    62  				Digest:       digest.NewFromBlob([]byte(tc.contents)),
    63  				IsExecutable: tc.executable,
    64  			}
    65  			if diff := cmp.Diff(want, got, ignoreMtime); diff != "" {
    66  				t.Errorf("Compute(%v) returned diff. (-want +got)\n%s", filename, diff)
    67  			}
    68  			if got.MTime.Before(before) || got.MTime.After(after) {
    69  				t.Errorf("Compute(%v) returned MTime %v, want time in (%v, %v).", filename, got.MTime, before, after)
    70  			}
    71  		})
    72  	}
    73  }
    74  
    75  func TestComputeFilesWithXattr(t *testing.T) {
    76  	overwriteXattrGlobals(t, "google.digest.sha256", xattributeAccessorMock{})
    77  	tests := []struct {
    78  		name       string
    79  		contents   string
    80  		executable bool
    81  	}{
    82  		{
    83  			name:     "empty",
    84  			contents: "",
    85  		},
    86  		{
    87  			name:     "non-executable",
    88  			contents: "bla",
    89  		},
    90  		{
    91  			name:       "executable",
    92  			contents:   "foo",
    93  			executable: true,
    94  		},
    95  	}
    96  	for _, tc := range tests {
    97  		t.Run(tc.name, func(t *testing.T) {
    98  			getXAttrMock = func(_ string, _ string) ([]byte, error) {
    99  				return []byte(mockedHash), nil
   100  			}
   101  
   102  			// Compare unix seconds rather than instants because Truncate operates on durations
   103  			// which means the returned instant is not always as expected.
   104  			before := time.Now().Unix()
   105  			filename, err := testutil.CreateFile(t, tc.executable, tc.contents)
   106  			if err != nil {
   107  				t.Fatalf("Failed to create tmp file for testing digests: %v", err)
   108  			}
   109  			after := time.Now().Add(time.Second).Unix()
   110  			t.Cleanup(func() { os.RemoveAll(filename) })
   111  			got := Compute(filename)
   112  			if got.Err != nil {
   113  				t.Errorf("Compute(%v) failed. Got error: %v", filename, got.Err)
   114  			}
   115  			wantDigest, err := digest.NewFromString(fmt.Sprintf("%s/%d", mockedHash, len(tc.contents)))
   116  			if err != nil {
   117  				t.Fatalf("Failed to create wantDigest: %v", err)
   118  			}
   119  			want := &Metadata{
   120  				Digest:       wantDigest,
   121  				IsExecutable: tc.executable,
   122  			}
   123  			if diff := cmp.Diff(want, got, ignoreMtime); diff != "" {
   124  				t.Errorf("Compute(%v) returned diff. (-want +got)\n%s", filename, diff)
   125  			}
   126  			gotMT := got.MTime.Unix()
   127  			if gotMT < before || gotMT > after {
   128  				t.Errorf("Compute(%v) returned MTime %v, want time between (%v, %v).", filename, gotMT, before, after)
   129  			}
   130  		})
   131  	}
   132  }
   133  
   134  func TestComputeFileDigestWithXattr(t *testing.T) {
   135  	xattrDgName := "user.myhash"
   136  	overwriteXattrDgName(t, xattrDgName)
   137  	tests := []struct {
   138  		name       string
   139  		contents   string
   140  		xattrDgStr string
   141  		wantDgStr  string
   142  		wantErr    bool
   143  	}{
   144  		{
   145  			name:      "no-xattr",
   146  			contents:  "123456",
   147  			wantDgStr: "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92/6",
   148  		},
   149  		{
   150  			name:       "only digest hash",
   151  			contents:   "123456",
   152  			xattrDgStr: "1111111111111111111111111111111111111111111111111111111111111111",
   153  			wantDgStr:  "1111111111111111111111111111111111111111111111111111111111111111/6",
   154  		},
   155  		{
   156  			name:       "full digest (hash+size)",
   157  			contents:   "",
   158  			xattrDgStr: "1111111111111111111111111111111111111111111111111111111111111111/666",
   159  			wantDgStr:  "1111111111111111111111111111111111111111111111111111111111111111/666",
   160  		},
   161  		{
   162  			name:       "invalid digest hash",
   163  			contents:   "123456",
   164  			xattrDgStr: "abc",
   165  			wantDgStr:  digest.Empty.String(),
   166  			wantErr:    true,
   167  		},
   168  		{
   169  			name:       "invalid full digest",
   170  			contents:   "123456",
   171  			xattrDgStr: "666/666",
   172  			wantDgStr:  digest.Empty.String(),
   173  			wantErr:    true,
   174  		},
   175  		{
   176  			name:       "invalid full digest (extra-slash)",
   177  			contents:   "123456",
   178  			xattrDgStr: "///666",
   179  			wantDgStr:  digest.Empty.String(),
   180  			wantErr:    true,
   181  		},
   182  	}
   183  
   184  	for _, tc := range tests {
   185  		t.Run(tc.name, func(t *testing.T) {
   186  			testName := tc.name
   187  			targetFilePath := createFileWithXattr(t, tc.contents, xattrDgName, tc.xattrDgStr)
   188  			t.Cleanup(func() { os.RemoveAll(targetFilePath) })
   189  			md := Compute(targetFilePath)
   190  			if tc.wantErr && md.Err == nil {
   191  				t.Errorf("No error while computing digest for test %v, but error was expected", testName)
   192  			}
   193  			if !tc.wantErr && md.Err != nil {
   194  				t.Errorf("Returned error while computing digest for test %v, err: %v", testName, md.Err)
   195  			}
   196  			got := md.Digest.String()
   197  			want := tc.wantDgStr
   198  			if diff := cmp.Diff(want, got); diff != "" {
   199  				t.Errorf("Compute Digest for test %v returned diff. (-want +got)\n%s", testName, diff)
   200  			}
   201  		})
   202  	}
   203  }
   204  
   205  func TestComputeDirectory(t *testing.T) {
   206  	tmpDir := t.TempDir()
   207  	got := Compute(tmpDir)
   208  	if got.Err != nil {
   209  		t.Errorf("Compute(%v).Err = %v, expected nil", tmpDir, got.Err)
   210  	}
   211  	if !got.IsDirectory {
   212  		t.Errorf("Compute(%v).IsDirectory = false, want true", tmpDir)
   213  	}
   214  	if got.Digest != digest.Empty {
   215  		t.Errorf("Compute(%v).Digest = %v, want %v", tmpDir, got.Digest, digest.Empty)
   216  	}
   217  }
   218  
   219  func TestComputeSymlinksToFile(t *testing.T) {
   220  	tests := []struct {
   221  		name       string
   222  		contents   string
   223  		executable bool
   224  	}{
   225  		{
   226  			name:     "valid",
   227  			contents: "bla",
   228  		},
   229  		{
   230  			name:       "valid-executable",
   231  			contents:   "executable",
   232  			executable: true,
   233  		},
   234  	}
   235  	for _, tc := range tests {
   236  		t.Run(tc.name, func(t *testing.T) {
   237  			symlinkPath := filepath.Join(os.TempDir(), tc.name)
   238  			t.Cleanup(func() { os.RemoveAll(symlinkPath) })
   239  			targetPath, err := createSymlinkToFile(t, symlinkPath, tc.executable, tc.contents)
   240  			if err != nil {
   241  				t.Fatalf("Failed to create tmp symlink for testing digests: %v", err)
   242  			}
   243  			got := Compute(symlinkPath)
   244  
   245  			if got.Err != nil {
   246  				t.Errorf("Compute(%v) failed. Got error: %v", symlinkPath, got.Err)
   247  			}
   248  			want := &Metadata{
   249  				Symlink: &SymlinkMetadata{
   250  					Target:     targetPath,
   251  					IsDangling: false,
   252  				},
   253  				Digest:       digest.NewFromBlob([]byte(tc.contents)),
   254  				IsExecutable: tc.executable,
   255  			}
   256  
   257  			if diff := cmp.Diff(want, got, ignoreMtime); diff != "" {
   258  				t.Errorf("Compute(%v) returned diff. (-want +got)\n%s", symlinkPath, diff)
   259  			}
   260  		})
   261  	}
   262  }
   263  
   264  func TestComputeDanglingSymlinks(t *testing.T) {
   265  	// Create a temporary fake target so that os.Symlink() can work.
   266  	symlinkPath := filepath.Join(os.TempDir(), "dangling")
   267  	t.Cleanup(func() { os.RemoveAll(symlinkPath) })
   268  	targetPath, err := createSymlinkToFile(t, symlinkPath, false, "transient")
   269  	if err != nil {
   270  		t.Fatalf("Failed to create tmp symlink for testing digests: %v", err)
   271  	}
   272  	// Make the symlink dangling
   273  	os.RemoveAll(targetPath)
   274  
   275  	got := Compute(symlinkPath)
   276  	if got.Err == nil || !got.Symlink.IsDangling {
   277  		t.Errorf("Compute(%v) should fail because the symlink is dangling", symlinkPath)
   278  	}
   279  	if got.Symlink.Target != targetPath {
   280  		t.Errorf("Compute(%v) should still set Target for the dangling symlink, want=%v got=%v", symlinkPath, targetPath, got.Symlink.Target)
   281  	}
   282  }
   283  
   284  func TestComputeSymlinksToDirectory(t *testing.T) {
   285  	symlinkPath := filepath.Join(os.TempDir(), "dir-symlink")
   286  	t.Cleanup(func() { os.RemoveAll(symlinkPath) })
   287  	targetPath := t.TempDir()
   288  	if err := createSymlinkToTarget(t, symlinkPath, targetPath); err != nil {
   289  		t.Fatalf("Failed to create tmp symlink for testing digests: %v", err)
   290  	}
   291  
   292  	got := Compute(symlinkPath)
   293  	if got.Err != nil {
   294  		t.Errorf("Compute(%v).Err = %v, expected nil", symlinkPath, got.Err)
   295  	}
   296  	if !got.IsDirectory {
   297  		t.Errorf("Compute(%v).IsDirectory = false, want true", symlinkPath)
   298  	}
   299  }
   300  
   301  func createFileWithXattr(t *testing.T, fileContent, xattrName, xattrValue string) string {
   302  	t.Helper()
   303  	filePath := filepath.Join(t.TempDir(), targetFile)
   304  	err := os.WriteFile(filePath, []byte(fileContent), 0666)
   305  	if err != nil {
   306  		t.Fatalf("Failed to write to a file: %v\n", err)
   307  	}
   308  	if xattrValue == "" {
   309  		return filePath
   310  	}
   311  	if err = xattr.Set(filePath, xattrName, []byte(xattrValue)); err == nil {
   312  		// setting xattr for a file in a TempDir succeeded
   313  		return filePath
   314  	}
   315  	// Setting xattr for a file in a TempDir might've failed because on some linux systems
   316  	// temp dir is mounted on tmpfs which does not support user extended attributes
   317  	// (https://man7.org/linux/man-pages/man5/tmpfs.5.html.
   318  	// In this case, try to create a file in a a working directory instead
   319  	t.Logf("Setting xattr for a file in %v failed. Using a working directory instead. err: %v",
   320  		t.TempDir(), err)
   321  	filePath = targetFile
   322  	if err = os.WriteFile(filePath, []byte(fileContent), 0666); err != nil {
   323  		t.Fatalf("Failed to write to a file: %v\n", err)
   324  	}
   325  	if err = xattr.Set(filePath, xattrName, []byte(xattrValue)); err == nil {
   326  		return filePath
   327  	}
   328  	os.RemoveAll(filePath)
   329  	// It's possible that the working directory is read only, skipping the test
   330  	// because it's not possible to set a user xattr neither in temp dir nor in working dir
   331  	// on a test environment
   332  	t.Logf("Setting xattr for a file in a working directory failed. Skipping the test. err: %v", err)
   333  	t.Skip("Cannot set a user xattr for a file in neither temp nor working directory")
   334  	return ""
   335  }
   336  
   337  func createSymlinkToFile(t *testing.T, symlinkPath string, executable bool, contents string) (string, error) {
   338  	t.Helper()
   339  	targetPath, err := testutil.CreateFile(t, executable, contents)
   340  	if err != nil {
   341  		return "", err
   342  	}
   343  	if err := createSymlinkToTarget(t, symlinkPath, targetPath); err != nil {
   344  		return "", err
   345  	}
   346  	return targetPath, nil
   347  }
   348  
   349  func createSymlinkToTarget(t *testing.T, symlinkPath string, targetPath string) error {
   350  	t.Helper()
   351  	return os.Symlink(targetPath, symlinkPath)
   352  }
   353  
   354  func overwriteXattrDgName(t *testing.T, newXattrDigestName string) {
   355  	t.Helper()
   356  	oldXattrDigestName := XattrDigestName
   357  	XattrDigestName = newXattrDigestName
   358  	t.Cleanup(func() {
   359  		XattrDigestName = oldXattrDigestName
   360  	})
   361  }
   362  
   363  func overwriteXattrGlobals(t *testing.T, newXattrDigestName string, newXattrAccess xattributeAccessorInterface) {
   364  	t.Helper()
   365  	oldXattrDigestName := XattrDigestName
   366  	oldXattrAccess := XattrAccess
   367  	XattrDigestName = newXattrDigestName
   368  	XattrAccess = newXattrAccess
   369  	t.Cleanup(func() {
   370  		XattrDigestName = oldXattrDigestName
   371  		XattrAccess = oldXattrAccess
   372  	})
   373  }
   374  
   375  // Mocking of the xattr package for testing.
   376  var getXAttrMock func(path string, name string) ([]byte, error)
   377  
   378  type xattributeAccessorMock struct{}
   379  
   380  func (x xattributeAccessorMock) getXAttr(path string, name string) ([]byte, error) {
   381  	return getXAttrMock(path, name)
   382  }
   383  
   384  func (x xattributeAccessorMock) isSupported() bool {
   385  	return true
   386  }