github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/source/directorysource/directory_source_test.go (about) 1 package directorysource 2 3 import ( 4 "io/fs" 5 "os" 6 "path/filepath" 7 "testing" 8 9 "github.com/google/go-cmp/cmp" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 13 "github.com/anchore/stereoscope/pkg/file" 14 "github.com/anchore/syft/syft/artifact" 15 "github.com/anchore/syft/syft/internal/fileresolver" 16 "github.com/anchore/syft/syft/internal/testutil" 17 "github.com/anchore/syft/syft/source" 18 ) 19 20 func TestNewFromDirectory(t *testing.T) { 21 testutil.Chdir(t, "..") // run with source/test-fixtures 22 23 testCases := []struct { 24 desc string 25 input string 26 expString string 27 inputPaths []string 28 expectedRefs int 29 cxErr require.ErrorAssertionFunc 30 }{ 31 { 32 desc: "no paths exist", 33 input: "foobar/", 34 inputPaths: []string{"/opt/", "/other"}, 35 cxErr: require.Error, 36 }, 37 { 38 desc: "path detected", 39 input: "test-fixtures", 40 inputPaths: []string{"path-detected/.vimrc"}, 41 expectedRefs: 1, 42 }, 43 { 44 desc: "directory ignored", 45 input: "test-fixtures", 46 inputPaths: []string{"path-detected"}, 47 expectedRefs: 0, 48 }, 49 { 50 desc: "no files-by-path detected", 51 input: "test-fixtures", 52 inputPaths: []string{"no-path-detected"}, 53 expectedRefs: 0, 54 }, 55 } 56 for _, test := range testCases { 57 t.Run(test.desc, func(t *testing.T) { 58 if test.cxErr == nil { 59 test.cxErr = require.NoError 60 } 61 src, err := New(Config{ 62 Path: test.input, 63 }) 64 test.cxErr(t, err) 65 if err != nil { 66 return 67 } 68 require.NoError(t, err) 69 t.Cleanup(func() { 70 require.NoError(t, src.Close()) 71 }) 72 assert.Equal(t, test.input, src.Describe().Metadata.(source.DirectoryMetadata).Path) 73 74 res, err := src.FileResolver(source.SquashedScope) 75 require.NoError(t, err) 76 77 refs, err := res.FilesByPath(test.inputPaths...) 78 require.NoError(t, err) 79 80 if len(refs) != test.expectedRefs { 81 t.Errorf("unexpected number of refs returned: %d != %d", len(refs), test.expectedRefs) 82 } 83 84 }) 85 } 86 } 87 88 func Test_DirectorySource_FilesByGlob(t *testing.T) { 89 testutil.Chdir(t, "..") // run with source/test-fixtures 90 91 testCases := []struct { 92 desc string 93 input string 94 glob string 95 expected int 96 }{ 97 { 98 input: "test-fixtures", 99 desc: "no matches", 100 glob: "bar/foo", 101 expected: 0, 102 }, 103 { 104 input: "test-fixtures/path-detected", 105 desc: "a single match", 106 glob: "**/*vimrc", 107 expected: 1, 108 }, 109 { 110 input: "test-fixtures/path-detected", 111 desc: "multiple matches", 112 glob: "**", 113 expected: 2, 114 }, 115 } 116 for _, test := range testCases { 117 t.Run(test.desc, func(t *testing.T) { 118 src, err := New(Config{Path: test.input}) 119 require.NoError(t, err) 120 121 res, err := src.FileResolver(source.SquashedScope) 122 require.NoError(t, err) 123 t.Cleanup(func() { 124 require.NoError(t, src.Close()) 125 }) 126 127 contents, err := res.FilesByGlob(test.glob) 128 require.NoError(t, err) 129 if len(contents) != test.expected { 130 t.Errorf("unexpected number of files found by glob (%s): %d != %d", test.glob, len(contents), test.expected) 131 } 132 133 }) 134 } 135 } 136 137 func Test_DirectorySource_Exclusions(t *testing.T) { 138 testutil.Chdir(t, "..") // run with source/test-fixtures 139 140 testCases := []struct { 141 desc string 142 input string 143 glob string 144 expected []string 145 exclusions []string 146 err bool 147 }{ 148 { 149 input: "test-fixtures/system_paths", 150 desc: "exclude everything", 151 glob: "**", 152 expected: nil, 153 exclusions: []string{"**/*"}, 154 }, 155 { 156 input: "test-fixtures/image-simple", 157 desc: "a single path excluded", 158 glob: "**", 159 expected: []string{ 160 "Dockerfile", 161 "file-1.txt", 162 "file-2.txt", 163 }, 164 exclusions: []string{"**/target/**"}, 165 }, 166 { 167 input: "test-fixtures/image-simple", 168 desc: "exclude explicit directory relative to the root", 169 glob: "**", 170 expected: []string{ 171 "Dockerfile", 172 "file-1.txt", 173 "file-2.txt", 174 //"target/really/nested/file-3.txt", // explicitly skipped 175 }, 176 exclusions: []string{"./target"}, 177 }, 178 { 179 input: "test-fixtures/image-simple", 180 desc: "exclude explicit file relative to the root", 181 glob: "**", 182 expected: []string{ 183 "Dockerfile", 184 //"file-1.txt", // explicitly skipped 185 "file-2.txt", 186 "target/really/nested/file-3.txt", 187 }, 188 exclusions: []string{"./file-1.txt"}, 189 }, 190 { 191 input: "test-fixtures/image-simple", 192 desc: "exclude wildcard relative to the root", 193 glob: "**", 194 expected: []string{ 195 "Dockerfile", 196 //"file-1.txt", // explicitly skipped 197 //"file-2.txt", // explicitly skipped 198 "target/really/nested/file-3.txt", 199 }, 200 exclusions: []string{"./*.txt"}, 201 }, 202 { 203 input: "test-fixtures/image-simple", 204 desc: "exclude files deeper", 205 glob: "**", 206 expected: []string{ 207 "Dockerfile", 208 "file-1.txt", 209 "file-2.txt", 210 //"target/really/nested/file-3.txt", // explicitly skipped 211 }, 212 exclusions: []string{"**/really/**"}, 213 }, 214 { 215 input: "test-fixtures/image-simple", 216 desc: "files excluded with extension", 217 glob: "**", 218 expected: []string{ 219 "Dockerfile", 220 //"file-1.txt", // explicitly skipped 221 //"file-2.txt", // explicitly skipped 222 //"target/really/nested/file-3.txt", // explicitly skipped 223 }, 224 exclusions: []string{"**/*.txt"}, 225 }, 226 { 227 input: "test-fixtures/image-simple", 228 desc: "keep files with different extensions", 229 glob: "**", 230 expected: []string{ 231 "Dockerfile", 232 "file-1.txt", 233 "file-2.txt", 234 "target/really/nested/file-3.txt", 235 }, 236 exclusions: []string{"**/target/**/*.jar"}, 237 }, 238 { 239 input: "test-fixtures/path-detected", 240 desc: "file directly excluded", 241 glob: "**", 242 expected: []string{ 243 ".vimrc", 244 }, 245 exclusions: []string{"**/empty"}, 246 }, 247 { 248 input: "test-fixtures/path-detected", 249 desc: "pattern error containing **/", 250 glob: "**", 251 expected: []string{ 252 ".vimrc", 253 }, 254 exclusions: []string{"/**/empty"}, 255 err: true, 256 }, 257 { 258 input: "test-fixtures/path-detected", 259 desc: "pattern error incorrect start", 260 glob: "**", 261 expected: []string{ 262 ".vimrc", 263 }, 264 exclusions: []string{"empty"}, 265 err: true, 266 }, 267 { 268 input: "test-fixtures/path-detected", 269 desc: "pattern error starting with /", 270 glob: "**", 271 expected: []string{ 272 ".vimrc", 273 }, 274 exclusions: []string{"/empty"}, 275 err: true, 276 }, 277 } 278 279 for _, test := range testCases { 280 t.Run(test.desc, func(t *testing.T) { 281 src, err := New(Config{ 282 Path: test.input, 283 Exclude: source.ExcludeConfig{ 284 Paths: test.exclusions, 285 }, 286 }) 287 require.NoError(t, err) 288 t.Cleanup(func() { 289 require.NoError(t, src.Close()) 290 }) 291 292 if test.err { 293 _, err = src.FileResolver(source.SquashedScope) 294 require.Error(t, err) 295 return 296 } 297 require.NoError(t, err) 298 299 res, err := src.FileResolver(source.SquashedScope) 300 require.NoError(t, err) 301 302 locations, err := res.FilesByGlob(test.glob) 303 require.NoError(t, err) 304 305 var actual []string 306 for _, l := range locations { 307 actual = append(actual, l.RealPath) 308 } 309 310 assert.ElementsMatchf(t, test.expected, actual, "diff \n"+cmp.Diff(test.expected, actual)) 311 }) 312 } 313 } 314 315 func Test_getDirectoryExclusionFunctions_crossPlatform(t *testing.T) { 316 testCases := []struct { 317 desc string 318 root string 319 path string 320 finfo os.FileInfo 321 exclude string 322 walkHint error 323 }{ 324 { 325 desc: "directory exclusion", 326 root: "/", 327 path: "/usr/var/lib", 328 exclude: "**/var/lib", 329 finfo: file.ManualInfo{ModeValue: os.ModeDir}, 330 walkHint: fs.SkipDir, 331 }, 332 { 333 desc: "no file info", 334 root: "/", 335 path: "/usr/var/lib", 336 exclude: "**/var/lib", 337 walkHint: fileresolver.ErrSkipPath, 338 }, 339 // linux specific tests... 340 { 341 desc: "linux doublestar", 342 root: "/usr", 343 path: "/usr/var/lib/etc.txt", 344 exclude: "**/*.txt", 345 finfo: file.ManualInfo{}, 346 walkHint: fileresolver.ErrSkipPath, 347 }, 348 { 349 desc: "linux relative", 350 root: "/usr/var/lib", 351 path: "/usr/var/lib/etc.txt", 352 exclude: "./*.txt", 353 finfo: file.ManualInfo{}, 354 355 walkHint: fileresolver.ErrSkipPath, 356 }, 357 { 358 desc: "linux one level", 359 root: "/usr", 360 path: "/usr/var/lib/etc.txt", 361 exclude: "*/*.txt", 362 finfo: file.ManualInfo{}, 363 walkHint: nil, 364 }, 365 // NOTE: since these tests will run in linux and macOS, the windows paths will be 366 // considered relative if they do not start with a forward slash and paths with backslashes 367 // won't be modified by the filepath.ToSlash call, so these are emulating the result of 368 // filepath.ToSlash usage 369 370 // windows specific tests... 371 { 372 desc: "windows doublestar", 373 root: "/C:/User/stuff", 374 path: "/C:/User/stuff/thing.txt", 375 exclude: "**/*.txt", 376 finfo: file.ManualInfo{}, 377 walkHint: fileresolver.ErrSkipPath, 378 }, 379 { 380 desc: "windows relative", 381 root: "/C:/User/stuff", 382 path: "/C:/User/stuff/thing.txt", 383 exclude: "./*.txt", 384 finfo: file.ManualInfo{}, 385 walkHint: fileresolver.ErrSkipPath, 386 }, 387 { 388 desc: "windows one level", 389 root: "/C:/User/stuff", 390 path: "/C:/User/stuff/thing.txt", 391 exclude: "*/*.txt", 392 finfo: file.ManualInfo{}, 393 walkHint: nil, 394 }, 395 } 396 397 for _, test := range testCases { 398 t.Run(test.desc, func(t *testing.T) { 399 fns, err := GetDirectoryExclusionFunctions(test.root, []string{test.exclude}) 400 require.NoError(t, err) 401 402 for _, f := range fns { 403 result := f("", test.path, test.finfo, nil) 404 require.Equal(t, test.walkHint, result) 405 } 406 }) 407 } 408 } 409 410 func Test_DirectorySource_FilesByPathDoesNotExist(t *testing.T) { 411 testutil.Chdir(t, "..") // run with source/test-fixtures 412 413 testCases := []struct { 414 desc string 415 input string 416 path string 417 expected string 418 }{ 419 { 420 input: "test-fixtures/path-detected", 421 desc: "path does not exist", 422 path: "foo", 423 }, 424 } 425 for _, test := range testCases { 426 t.Run(test.desc, func(t *testing.T) { 427 src, err := New(Config{Path: test.input}) 428 require.NoError(t, err) 429 t.Cleanup(func() { 430 require.NoError(t, src.Close()) 431 }) 432 433 res, err := src.FileResolver(source.SquashedScope) 434 require.NoError(t, err) 435 436 refs, err := res.FilesByPath(test.path) 437 require.NoError(t, err) 438 439 assert.Len(t, refs, 0) 440 }) 441 } 442 } 443 444 func Test_DirectorySource_ID(t *testing.T) { 445 testutil.Chdir(t, "..") // run with source/test-fixtures 446 447 tests := []struct { 448 name string 449 cfg Config 450 want artifact.ID 451 wantErr require.ErrorAssertionFunc 452 }{ 453 { 454 name: "empty", 455 cfg: Config{}, 456 wantErr: require.Error, 457 }, 458 { 459 name: "to a non-existent directory", 460 cfg: Config{ 461 Path: "./test-fixtures/does-not-exist", 462 }, 463 wantErr: require.Error, 464 }, 465 { 466 name: "with odd unclean path through non-existent directory", 467 cfg: Config{Path: "test-fixtures/does-not-exist/../"}, 468 wantErr: require.Error, 469 }, 470 { 471 name: "to a file (not a directory)", 472 cfg: Config{ 473 Path: "./test-fixtures/image-simple/Dockerfile", 474 }, 475 wantErr: require.Error, 476 }, 477 { 478 name: "to dir with name and version", 479 cfg: Config{ 480 Path: "./test-fixtures", 481 Alias: source.Alias{ 482 Name: "name-me-that!", 483 Version: "version-me-this!", 484 }, 485 }, 486 want: artifact.ID("51a5f2a1536cf4b5220d4247814b07eec5862ab0547050f90e9ae216548ded7e"), 487 }, 488 { 489 name: "to different dir with name and version", 490 cfg: Config{ 491 Path: "./test-fixtures/image-simple", 492 Alias: source.Alias{ 493 Name: "name-me-that!", 494 Version: "version-me-this!", 495 }, 496 }, 497 // note: this must match the previous value because the alias should trump the path info 498 want: artifact.ID("51a5f2a1536cf4b5220d4247814b07eec5862ab0547050f90e9ae216548ded7e"), 499 }, 500 { 501 name: "with path", 502 cfg: Config{Path: "./test-fixtures"}, 503 want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"), 504 }, 505 { 506 name: "with unclean path", 507 cfg: Config{Path: "test-fixtures/image-simple/../"}, 508 want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"), 509 }, 510 { 511 name: "other fields do not affect ID", 512 cfg: Config{ 513 Path: "test-fixtures", 514 Base: "a-base!", 515 Exclude: source.ExcludeConfig{ 516 Paths: []string{"a", "b"}, 517 }, 518 }, 519 want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"), 520 }, 521 } 522 for _, tt := range tests { 523 t.Run(tt.name, func(t *testing.T) { 524 if tt.wantErr == nil { 525 tt.wantErr = require.NoError 526 } 527 s, err := New(tt.cfg) 528 tt.wantErr(t, err) 529 if err != nil { 530 return 531 } 532 assert.Equalf(t, tt.want, s.ID(), "ID()") 533 }) 534 } 535 } 536 537 func Test_cleanDirPath(t *testing.T) { 538 testutil.Chdir(t, "..") // run with source/test-fixtures 539 540 abs, err := filepath.Abs("test-fixtures") 541 require.NoError(t, err) 542 543 tests := []struct { 544 name string 545 path string 546 base string 547 want string 548 }{ 549 { 550 name: "abs path, abs base, base contained in path", 551 path: filepath.Join(abs, "system_paths/outside_root"), 552 base: abs, 553 want: "system_paths/outside_root", 554 }, 555 { 556 name: "abs path, abs base, base not contained in path", 557 path: "/var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/001/some/path", 558 base: "/var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/002", 559 want: "/var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/001/some/path", 560 }, 561 { 562 name: "path and base match", 563 path: "/var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/001/some/path", 564 base: "/var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/001/some/path", 565 want: "/var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/001/some/path", 566 }, 567 } 568 for _, tt := range tests { 569 t.Run(tt.name, func(t *testing.T) { 570 assert.Equal(t, tt.want, cleanDirPath(tt.path, tt.base)) 571 }) 572 } 573 }