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 }