github.com/anchore/syft@v1.38.2/syft/format/syftjson/to_format_model_test.go (about) 1 package syftjson 2 3 import ( 4 "encoding/json" 5 "testing" 6 7 "github.com/google/go-cmp/cmp" 8 "github.com/google/go-cmp/cmp/cmpopts" 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 12 stereoscopeFile "github.com/anchore/stereoscope/pkg/file" 13 "github.com/anchore/syft/internal/sourcemetadata" 14 "github.com/anchore/syft/syft/file" 15 "github.com/anchore/syft/syft/format/syftjson/model" 16 "github.com/anchore/syft/syft/pkg" 17 "github.com/anchore/syft/syft/source" 18 ) 19 20 func Test_toSourceModel_IgnoreBase(t *testing.T) { 21 tests := []struct { 22 name string 23 src source.Description 24 }{ 25 { 26 name: "directory", 27 src: source.Description{ 28 ID: "test-id", 29 Metadata: source.DirectoryMetadata{ 30 Path: "some/path", 31 Base: "some/base", 32 }, 33 }, 34 }, 35 } 36 for _, test := range tests { 37 t.Run(test.name, func(t *testing.T) { 38 // assert the model transformation is correct 39 actual := toSourceModel(test.src) 40 41 by, err := json.Marshal(actual) 42 require.NoError(t, err) 43 assert.NotContains(t, string(by), "some/base") 44 }) 45 } 46 } 47 48 func Test_toSourceModel(t *testing.T) { 49 tracker := sourcemetadata.NewCompletionTester(t) 50 51 tests := []struct { 52 name string 53 src source.Description 54 expected model.Source 55 }{ 56 { 57 name: "directory", 58 src: source.Description{ 59 ID: "test-id", 60 Name: "some-name", 61 Version: "some-version", 62 Supplier: "optional-supplier", 63 Metadata: source.DirectoryMetadata{ 64 Path: "some/path", 65 Base: "some/base", 66 }, 67 }, 68 expected: model.Source{ 69 ID: "test-id", 70 Name: "some-name", 71 Version: "some-version", 72 Supplier: "optional-supplier", 73 Type: "directory", 74 Metadata: source.DirectoryMetadata{ 75 Path: "some/path", 76 Base: "some/base", 77 }, 78 }, 79 }, 80 { 81 name: "file", 82 src: source.Description{ 83 ID: "test-id", 84 Name: "some-name", 85 Version: "some-version", 86 Supplier: "optional-supplier", 87 Metadata: source.FileMetadata{ 88 Path: "some/path", 89 Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}}, 90 MIMEType: "text/plain", 91 }, 92 }, 93 expected: model.Source{ 94 ID: "test-id", 95 Name: "some-name", 96 Version: "some-version", 97 Supplier: "optional-supplier", 98 Type: "file", 99 Metadata: source.FileMetadata{ 100 Path: "some/path", 101 Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}}, 102 MIMEType: "text/plain", 103 }, 104 }, 105 }, 106 { 107 name: "image", 108 src: source.Description{ 109 ID: "test-id", 110 Name: "some-name", 111 Version: "some-version", 112 Metadata: source.ImageMetadata{ 113 UserInput: "user-input", 114 ID: "id...", 115 ManifestDigest: "digest...", 116 MediaType: "type...", 117 }, 118 }, 119 expected: model.Source{ 120 ID: "test-id", 121 Name: "some-name", 122 Version: "some-version", 123 Type: "image", 124 Metadata: source.ImageMetadata{ 125 UserInput: "user-input", 126 ID: "id...", 127 ManifestDigest: "digest...", 128 MediaType: "type...", 129 RepoDigests: []string{}, 130 Tags: []string{}, 131 }, 132 }, 133 }, 134 { 135 name: "snap", 136 src: source.Description{ 137 ID: "test-id", 138 Name: "some-name", 139 Version: "some-version", 140 Metadata: source.SnapMetadata{ 141 Summary: "some summary", 142 Base: "some/base", 143 Grade: "some grade", 144 Confinement: "some confinement", 145 Architectures: []string{"x86_64", "arm64"}, 146 Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}}, 147 }, 148 }, 149 expected: model.Source{ 150 ID: "test-id", 151 Name: "some-name", 152 Version: "some-version", 153 Type: "snap", 154 Metadata: source.SnapMetadata{ 155 Summary: "some summary", 156 Base: "some/base", 157 Grade: "some grade", 158 Confinement: "some confinement", 159 Architectures: []string{"x86_64", "arm64"}, 160 Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}}, 161 }, 162 }, 163 }, 164 // below are regression tests for when the name/version are not provided 165 // historically we've hoisted up the name/version from the metadata, now it is a simple pass-through 166 { 167 name: "directory - no name/version", 168 src: source.Description{ 169 ID: "test-id", 170 Metadata: source.DirectoryMetadata{ 171 Path: "some/path", 172 Base: "some/base", 173 }, 174 }, 175 expected: model.Source{ 176 ID: "test-id", 177 Type: "directory", 178 Metadata: source.DirectoryMetadata{ 179 Path: "some/path", 180 Base: "some/base", 181 }, 182 }, 183 }, 184 { 185 name: "file - no name/version", 186 src: source.Description{ 187 ID: "test-id", 188 Metadata: source.FileMetadata{ 189 Path: "some/path", 190 Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}}, 191 MIMEType: "text/plain", 192 }, 193 }, 194 expected: model.Source{ 195 ID: "test-id", 196 Type: "file", 197 Metadata: source.FileMetadata{ 198 Path: "some/path", 199 Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}}, 200 MIMEType: "text/plain", 201 }, 202 }, 203 }, 204 { 205 name: "image - no name/version", 206 src: source.Description{ 207 ID: "test-id", 208 Metadata: source.ImageMetadata{ 209 UserInput: "user-input", 210 ID: "id...", 211 ManifestDigest: "digest...", 212 MediaType: "type...", 213 }, 214 }, 215 expected: model.Source{ 216 ID: "test-id", 217 Type: "image", 218 Metadata: source.ImageMetadata{ 219 UserInput: "user-input", 220 ID: "id...", 221 ManifestDigest: "digest...", 222 MediaType: "type...", 223 RepoDigests: []string{}, 224 Tags: []string{}, 225 }, 226 }, 227 }, 228 } 229 for _, test := range tests { 230 t.Run(test.name, func(t *testing.T) { 231 // assert the model transformation is correct 232 actual := toSourceModel(test.src) 233 assert.Equal(t, test.expected, actual) 234 235 // track each scheme tested (passed or not) 236 tracker.Tested(t, test.expected.Metadata) 237 }) 238 } 239 } 240 241 func Test_toFileType(t *testing.T) { 242 243 badType := stereoscopeFile.Type(0x1337) 244 var allTypesTested []stereoscopeFile.Type 245 tests := []struct { 246 ty stereoscopeFile.Type 247 name string 248 }{ 249 { 250 ty: stereoscopeFile.TypeRegular, 251 name: "RegularFile", 252 }, 253 { 254 ty: stereoscopeFile.TypeDirectory, 255 name: "Directory", 256 }, 257 { 258 ty: stereoscopeFile.TypeSymLink, 259 name: "SymbolicLink", 260 }, 261 { 262 ty: stereoscopeFile.TypeHardLink, 263 name: "HardLink", 264 }, 265 { 266 ty: stereoscopeFile.TypeSocket, 267 name: "Socket", 268 }, 269 { 270 ty: stereoscopeFile.TypeCharacterDevice, 271 name: "CharacterDevice", 272 }, 273 { 274 ty: stereoscopeFile.TypeBlockDevice, 275 name: "BlockDevice", 276 }, 277 { 278 ty: stereoscopeFile.TypeFIFO, 279 name: "FIFONode", 280 }, 281 { 282 ty: stereoscopeFile.TypeIrregular, 283 name: "IrregularFile", 284 }, 285 { 286 ty: badType, 287 name: "Unknown", 288 }, 289 } 290 for _, tt := range tests { 291 t.Run(tt.name, func(t *testing.T) { 292 assert.Equalf(t, tt.name, toFileType(tt.ty), "toFileType(%v)", tt.ty) 293 if tt.ty != badType { 294 allTypesTested = append(allTypesTested, tt.ty) 295 } 296 }) 297 } 298 299 assert.ElementsMatch(t, allTypesTested, stereoscopeFile.AllTypes(), "not all file.Types are under test") 300 } 301 302 func Test_toFileMetadataEntry(t *testing.T) { 303 coords := file.Coordinates{ 304 RealPath: "/path", 305 FileSystemID: "x", 306 } 307 tests := []struct { 308 name string 309 metadata *file.Metadata 310 want *model.FileMetadataEntry 311 }{ 312 { 313 name: "no metadata", 314 }, 315 { 316 name: "no file info", 317 metadata: &file.Metadata{ 318 FileInfo: nil, 319 }, 320 want: &model.FileMetadataEntry{ 321 Type: stereoscopeFile.TypeRegular.String(), 322 }, 323 }, 324 { 325 name: "with file info", 326 metadata: &file.Metadata{ 327 FileInfo: &stereoscopeFile.ManualInfo{ 328 ModeValue: 1, 329 }, 330 }, 331 want: &model.FileMetadataEntry{ 332 Mode: 1, 333 Type: stereoscopeFile.TypeRegular.String(), 334 }, 335 }, 336 } 337 for _, tt := range tests { 338 t.Run(tt.name, func(t *testing.T) { 339 assert.Equal(t, tt.want, toFileMetadataEntry(coords, tt.metadata)) 340 }) 341 } 342 } 343 344 func Test_toPackageModel_metadataType(t *testing.T) { 345 tests := []struct { 346 name string 347 p pkg.Package 348 cfg EncoderConfig 349 want model.Package 350 }{ 351 { 352 name: "empty config", 353 p: pkg.Package{ 354 Metadata: pkg.RpmDBEntry{}, 355 }, 356 cfg: EncoderConfig{}, 357 want: model.Package{ 358 PackageCustomData: model.PackageCustomData{ 359 MetadataType: "rpm-db-entry", 360 Metadata: pkg.RpmDBEntry{}, 361 }, 362 }, 363 }, 364 { 365 name: "legacy config", 366 p: pkg.Package{ 367 Metadata: pkg.RpmDBEntry{}, 368 }, 369 cfg: EncoderConfig{ 370 Legacy: true, 371 }, 372 want: model.Package{ 373 PackageCustomData: model.PackageCustomData{ 374 MetadataType: "RpmMetadata", 375 Metadata: pkg.RpmDBEntry{}, 376 }, 377 }, 378 }, 379 } 380 for _, tt := range tests { 381 t.Run(tt.name, func(t *testing.T) { 382 if d := cmp.Diff(tt.want, toPackageModel(tt.p, file.LocationSorter(nil), tt.cfg), cmpopts.EquateEmpty()); d != "" { 383 t.Errorf("unexpected package (-want +got):\n%s", d) 384 } 385 }) 386 } 387 } 388 389 func Test_toPackageModel_layerOrdering(t *testing.T) { 390 tests := []struct { 391 name string 392 p pkg.Package 393 layerOrder []string 394 cfg EncoderConfig 395 want model.Package 396 }{ 397 { 398 name: "with layer ordering", 399 p: pkg.Package{ 400 Name: "pkg-1", 401 Licenses: pkg.NewLicenseSet(pkg.License{ 402 Value: "MIT", 403 Locations: file.NewLocationSet( 404 file.NewLocationFromCoordinates(file.Coordinates{ 405 RealPath: "/lic-a", 406 FileSystemID: "fsid-3", 407 }), 408 file.NewLocationFromCoordinates(file.Coordinates{ 409 RealPath: "/lic-a", 410 FileSystemID: "fsid-1", 411 }), 412 file.NewLocationFromCoordinates(file.Coordinates{ 413 RealPath: "/lic-b", 414 FileSystemID: "fsid-0", 415 }), 416 file.NewLocationFromCoordinates(file.Coordinates{ 417 RealPath: "/lic-a", 418 FileSystemID: "fsid-2", 419 }), 420 ), 421 }), 422 Locations: file.NewLocationSet( 423 file.NewLocationFromCoordinates(file.Coordinates{ 424 RealPath: "/a", 425 FileSystemID: "fsid-3", 426 }), 427 file.NewLocationFromCoordinates(file.Coordinates{ 428 RealPath: "/a", 429 FileSystemID: "fsid-1", 430 }), 431 file.NewLocationFromCoordinates(file.Coordinates{ 432 RealPath: "/b", 433 FileSystemID: "fsid-0", 434 }), 435 file.NewLocationFromCoordinates(file.Coordinates{ 436 RealPath: "/a", 437 FileSystemID: "fsid-2", 438 }), 439 ), 440 }, 441 layerOrder: []string{ 442 "fsid-0", 443 "fsid-1", 444 "fsid-2", 445 "fsid-3", 446 }, 447 want: model.Package{ 448 PackageBasicData: model.PackageBasicData{ 449 Name: "pkg-1", 450 Licenses: []model.License{ 451 { 452 Value: "MIT", 453 Locations: []file.Location{ 454 { 455 LocationData: file.LocationData{ 456 Coordinates: file.Coordinates{ 457 RealPath: "/lic-b", 458 FileSystemID: "fsid-0", // important! 459 }, 460 AccessPath: "/lic-b", 461 }, 462 }, 463 { 464 LocationData: file.LocationData{ 465 Coordinates: file.Coordinates{ 466 RealPath: "/lic-a", 467 FileSystemID: "fsid-1", // important! 468 }, 469 AccessPath: "/lic-a", 470 }, 471 }, 472 { 473 LocationData: file.LocationData{ 474 Coordinates: file.Coordinates{ 475 RealPath: "/lic-a", 476 FileSystemID: "fsid-2", // important! 477 }, 478 AccessPath: "/lic-a", 479 }, 480 }, 481 { 482 LocationData: file.LocationData{ 483 Coordinates: file.Coordinates{ 484 RealPath: "/lic-a", 485 FileSystemID: "fsid-3", // important! 486 }, 487 AccessPath: "/lic-a", 488 }, 489 }, 490 }, 491 }, 492 }, 493 Locations: []file.Location{ 494 { 495 LocationData: file.LocationData{ 496 Coordinates: file.Coordinates{ 497 RealPath: "/b", 498 FileSystemID: "fsid-0", // important! 499 }, 500 AccessPath: "/b", 501 }, 502 }, 503 { 504 LocationData: file.LocationData{ 505 Coordinates: file.Coordinates{ 506 RealPath: "/a", 507 FileSystemID: "fsid-1", // important! 508 }, 509 AccessPath: "/a", 510 }, 511 }, 512 { 513 LocationData: file.LocationData{ 514 Coordinates: file.Coordinates{ 515 RealPath: "/a", 516 FileSystemID: "fsid-2", // important! 517 }, 518 AccessPath: "/a", 519 }, 520 }, 521 { 522 LocationData: file.LocationData{ 523 Coordinates: file.Coordinates{ 524 RealPath: "/a", 525 FileSystemID: "fsid-3", // important! 526 }, 527 AccessPath: "/a", 528 }, 529 }, 530 }, 531 }, 532 }, 533 }, 534 } 535 for _, tt := range tests { 536 t.Run(tt.name, func(t *testing.T) { 537 if d := cmp.Diff(tt.want, toPackageModel(tt.p, file.LocationSorter(tt.layerOrder), tt.cfg), cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(file.LocationData{})); d != "" { 538 t.Errorf("unexpected package (-want +got):\n%s", d) 539 } 540 }) 541 } 542 } 543 544 func Test_toLocationModel(t *testing.T) { 545 tests := []struct { 546 name string 547 locations file.LocationSet 548 layers []string 549 want []file.Location 550 }{ 551 { 552 name: "empty location set", 553 locations: file.NewLocationSet(), 554 layers: []string{"fsid-1"}, 555 want: []file.Location{}, 556 }, 557 { 558 name: "nil layer order map", 559 locations: file.NewLocationSet( 560 file.NewLocationFromCoordinates(file.Coordinates{ 561 RealPath: "/a", 562 FileSystemID: "fsid-1", 563 }), 564 file.NewLocationFromCoordinates(file.Coordinates{ 565 RealPath: "/b", 566 FileSystemID: "fsid-2", 567 }), 568 ), 569 layers: nil, // please don't panic! 570 want: []file.Location{ 571 { 572 LocationData: file.LocationData{ 573 Coordinates: file.Coordinates{ 574 RealPath: "/a", 575 FileSystemID: "fsid-1", 576 }, 577 AccessPath: "/a", 578 }, 579 LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}}, 580 }, 581 { 582 LocationData: file.LocationData{ 583 Coordinates: file.Coordinates{ 584 RealPath: "/b", 585 FileSystemID: "fsid-2", 586 }, 587 AccessPath: "/b", 588 }, 589 LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}}, 590 }, 591 }, 592 }, 593 { 594 name: "go case", 595 locations: file.NewLocationSet( 596 file.NewLocationFromCoordinates(file.Coordinates{ 597 RealPath: "/a", 598 FileSystemID: "fsid-3", 599 }), 600 file.NewLocationFromCoordinates(file.Coordinates{ 601 RealPath: "/b", 602 FileSystemID: "fsid-1", 603 }), 604 file.NewLocationFromCoordinates(file.Coordinates{ 605 RealPath: "/c", 606 FileSystemID: "fsid-2", 607 }), 608 ), 609 layers: []string{ 610 "fsid-1", 611 "fsid-2", 612 "fsid-3", 613 }, 614 want: []file.Location{ 615 { 616 LocationData: file.LocationData{ 617 Coordinates: file.Coordinates{ 618 RealPath: "/b", 619 FileSystemID: "fsid-1", 620 }, 621 AccessPath: "/b", 622 }, 623 LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}}, 624 }, 625 { 626 LocationData: file.LocationData{ 627 Coordinates: file.Coordinates{ 628 RealPath: "/c", 629 FileSystemID: "fsid-2", 630 }, 631 AccessPath: "/c", 632 }, 633 LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}}, 634 }, 635 { 636 LocationData: file.LocationData{ 637 Coordinates: file.Coordinates{ 638 RealPath: "/a", 639 FileSystemID: "fsid-3", 640 }, 641 AccessPath: "/a", 642 }, 643 LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}}, 644 }, 645 }, 646 }, 647 { 648 name: "same layer different paths", // prove we can sort by path irrespective of layer 649 locations: file.NewLocationSet( 650 file.NewLocationFromCoordinates(file.Coordinates{ 651 RealPath: "/c", 652 FileSystemID: "fsid-1", 653 }), 654 file.NewLocationFromCoordinates(file.Coordinates{ 655 RealPath: "/a", 656 FileSystemID: "fsid-1", 657 }), 658 file.NewLocationFromCoordinates(file.Coordinates{ 659 RealPath: "/b", 660 FileSystemID: "fsid-1", 661 }), 662 ), 663 layers: []string{ 664 "fsid-1", 665 }, 666 want: []file.Location{ 667 { 668 LocationData: file.LocationData{ 669 Coordinates: file.Coordinates{ 670 RealPath: "/a", 671 FileSystemID: "fsid-1", 672 }, 673 AccessPath: "/a", 674 }, 675 LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}}, 676 }, 677 { 678 LocationData: file.LocationData{ 679 Coordinates: file.Coordinates{ 680 RealPath: "/b", 681 FileSystemID: "fsid-1", 682 }, 683 AccessPath: "/b", 684 }, 685 LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}}, 686 }, 687 { 688 LocationData: file.LocationData{ 689 Coordinates: file.Coordinates{ 690 RealPath: "/c", 691 FileSystemID: "fsid-1", 692 }, 693 AccessPath: "/c", 694 }, 695 LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}}, 696 }, 697 }, 698 }, 699 { 700 name: "mixed layers and paths", 701 locations: file.NewLocationSet( 702 file.NewLocationFromCoordinates(file.Coordinates{ 703 RealPath: "/z", 704 FileSystemID: "fsid-3", 705 }), 706 file.NewLocationFromCoordinates(file.Coordinates{ 707 RealPath: "/a", 708 FileSystemID: "fsid-2", 709 }), 710 file.NewLocationFromCoordinates(file.Coordinates{ 711 RealPath: "/b", 712 FileSystemID: "fsid-1", 713 }), 714 file.NewLocationFromCoordinates(file.Coordinates{ 715 RealPath: "/c", 716 FileSystemID: "fsid-2", 717 }), 718 ), 719 layers: []string{ 720 "fsid-1", 721 "fsid-2", 722 "fsid-3", 723 }, 724 want: []file.Location{ 725 { 726 LocationData: file.LocationData{ 727 Coordinates: file.Coordinates{ 728 RealPath: "/b", 729 FileSystemID: "fsid-1", 730 }, 731 AccessPath: "/b", 732 }, 733 LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}}, 734 }, 735 { 736 LocationData: file.LocationData{ 737 Coordinates: file.Coordinates{ 738 RealPath: "/a", 739 FileSystemID: "fsid-2", 740 }, 741 AccessPath: "/a", 742 }, 743 LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}}, 744 }, 745 { 746 LocationData: file.LocationData{ 747 Coordinates: file.Coordinates{ 748 RealPath: "/c", 749 FileSystemID: "fsid-2", 750 }, 751 AccessPath: "/c", 752 }, 753 LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}}, 754 }, 755 { 756 LocationData: file.LocationData{ 757 Coordinates: file.Coordinates{ 758 RealPath: "/z", 759 FileSystemID: "fsid-3", 760 }, 761 AccessPath: "/z", 762 }, 763 LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}}, 764 }, 765 }, 766 }, 767 } 768 769 for _, tt := range tests { 770 t.Run(tt.name, func(t *testing.T) { 771 got := toLocationsModel(tt.locations, file.LocationSorter(tt.layers)) 772 if d := cmp.Diff(tt.want, got, cmpopts.IgnoreUnexported(file.LocationData{})); d != "" { 773 t.Errorf("toLocationsModel() mismatch (-want +got):\n%s", d) 774 } 775 }) 776 } 777 } 778 779 func Test_sortFiles(t *testing.T) { 780 tests := []struct { 781 name string 782 files []model.File 783 layers []string 784 want []model.File 785 }{ 786 { 787 name: "empty files slice", 788 files: []model.File{}, 789 layers: []string{"fsid-1"}, 790 want: []model.File{}, 791 }, 792 { 793 name: "nil layer order map", 794 files: []model.File{ 795 { 796 ID: "file-1", 797 Location: file.Coordinates{ 798 RealPath: "/a", 799 FileSystemID: "fsid-1", 800 }, 801 }, 802 { 803 ID: "file-2", 804 Location: file.Coordinates{ 805 RealPath: "/b", 806 FileSystemID: "fsid-2", 807 }, 808 }, 809 }, 810 layers: nil, 811 want: []model.File{ 812 { 813 ID: "file-1", 814 Location: file.Coordinates{ 815 RealPath: "/a", 816 FileSystemID: "fsid-1", 817 }, 818 }, 819 { 820 ID: "file-2", 821 Location: file.Coordinates{ 822 RealPath: "/b", 823 FileSystemID: "fsid-2", 824 }, 825 }, 826 }, 827 }, 828 { 829 name: "layer ordering", 830 files: []model.File{ 831 { 832 ID: "file-1", 833 Location: file.Coordinates{ 834 RealPath: "/a", 835 FileSystemID: "fsid-3", 836 }, 837 }, 838 { 839 ID: "file-2", 840 Location: file.Coordinates{ 841 RealPath: "/b", 842 FileSystemID: "fsid-1", 843 }, 844 }, 845 { 846 ID: "file-3", 847 Location: file.Coordinates{ 848 RealPath: "/c", 849 FileSystemID: "fsid-2", 850 }, 851 }, 852 }, 853 layers: []string{ 854 "fsid-1", 855 "fsid-2", 856 "fsid-3", 857 }, 858 want: []model.File{ 859 { 860 ID: "file-2", 861 Location: file.Coordinates{ 862 RealPath: "/b", 863 FileSystemID: "fsid-1", 864 }, 865 }, 866 { 867 ID: "file-3", 868 Location: file.Coordinates{ 869 RealPath: "/c", 870 FileSystemID: "fsid-2", 871 }, 872 }, 873 { 874 ID: "file-1", 875 Location: file.Coordinates{ 876 RealPath: "/a", 877 FileSystemID: "fsid-3", 878 }, 879 }, 880 }, 881 }, 882 { 883 name: "same layer different paths", 884 files: []model.File{ 885 { 886 ID: "file-1", 887 Location: file.Coordinates{ 888 RealPath: "/c", 889 FileSystemID: "fsid-1", 890 }, 891 }, 892 { 893 ID: "file-2", 894 Location: file.Coordinates{ 895 RealPath: "/a", 896 FileSystemID: "fsid-1", 897 }, 898 }, 899 { 900 ID: "file-3", 901 Location: file.Coordinates{ 902 RealPath: "/b", 903 FileSystemID: "fsid-1", 904 }, 905 }, 906 }, 907 layers: []string{ 908 "fsid-1", 909 }, 910 want: []model.File{ 911 { 912 ID: "file-2", 913 Location: file.Coordinates{ 914 RealPath: "/a", 915 FileSystemID: "fsid-1", 916 }, 917 }, 918 { 919 ID: "file-3", 920 Location: file.Coordinates{ 921 RealPath: "/b", 922 FileSystemID: "fsid-1", 923 }, 924 }, 925 { 926 ID: "file-1", 927 Location: file.Coordinates{ 928 RealPath: "/c", 929 FileSystemID: "fsid-1", 930 }, 931 }, 932 }, 933 }, 934 { 935 name: "stability test - preserve original order for equivalent items", 936 files: []model.File{ 937 { 938 ID: "file-1", 939 Location: file.Coordinates{ 940 RealPath: "/a", 941 FileSystemID: "fsid-1", 942 }, 943 }, 944 { 945 ID: "file-2", 946 Location: file.Coordinates{ 947 RealPath: "/a", 948 FileSystemID: "fsid-1", 949 }, 950 }, 951 { 952 ID: "file-3", 953 Location: file.Coordinates{ 954 RealPath: "/a", 955 FileSystemID: "fsid-1", 956 }, 957 }, 958 }, 959 layers: []string{ 960 "fsid-1", 961 }, 962 want: []model.File{ 963 { 964 ID: "file-1", 965 Location: file.Coordinates{ 966 RealPath: "/a", 967 FileSystemID: "fsid-1", 968 }, 969 }, 970 { 971 ID: "file-2", 972 Location: file.Coordinates{ 973 RealPath: "/a", 974 FileSystemID: "fsid-1", 975 }, 976 }, 977 { 978 ID: "file-3", 979 Location: file.Coordinates{ 980 RealPath: "/a", 981 FileSystemID: "fsid-1", 982 }, 983 }, 984 }, 985 }, 986 { 987 name: "complex file metadata doesn't affect sorting", 988 files: []model.File{ 989 { 990 ID: "file-1", 991 Location: file.Coordinates{ 992 RealPath: "/a", 993 FileSystemID: "fsid-2", 994 }, 995 Metadata: &model.FileMetadataEntry{ 996 Mode: 0644, 997 Type: "file", 998 UserID: 1000, 999 GroupID: 1000, 1000 MIMEType: "text/plain", 1001 Size: 100, 1002 }, 1003 Contents: "content1", 1004 Digests: []file.Digest{ 1005 { 1006 Algorithm: "sha256", 1007 Value: "abc123", 1008 }, 1009 }, 1010 }, 1011 { 1012 ID: "file-2", 1013 Location: file.Coordinates{ 1014 RealPath: "/b", 1015 FileSystemID: "fsid-1", 1016 }, 1017 Metadata: &model.FileMetadataEntry{ 1018 Mode: 0755, 1019 Type: "directory", 1020 UserID: 0, 1021 GroupID: 0, 1022 MIMEType: "application/directory", 1023 Size: 4096, 1024 }, 1025 }, 1026 }, 1027 layers: []string{ 1028 "fsid-1", 1029 "fsid-2", 1030 }, 1031 want: []model.File{ 1032 { 1033 ID: "file-2", 1034 Location: file.Coordinates{ 1035 RealPath: "/b", 1036 FileSystemID: "fsid-1", 1037 }, 1038 Metadata: &model.FileMetadataEntry{ 1039 Mode: 0755, 1040 Type: "directory", 1041 UserID: 0, 1042 GroupID: 0, 1043 MIMEType: "application/directory", 1044 Size: 4096, 1045 }, 1046 }, 1047 { 1048 ID: "file-1", 1049 Location: file.Coordinates{ 1050 RealPath: "/a", 1051 FileSystemID: "fsid-2", 1052 }, 1053 Metadata: &model.FileMetadataEntry{ 1054 Mode: 0644, 1055 Type: "file", 1056 UserID: 1000, 1057 GroupID: 1000, 1058 MIMEType: "text/plain", 1059 Size: 100, 1060 }, 1061 Contents: "content1", 1062 Digests: []file.Digest{ 1063 { 1064 Algorithm: "sha256", 1065 Value: "abc123", 1066 }, 1067 }, 1068 }, 1069 }, 1070 }, 1071 } 1072 1073 for _, tt := range tests { 1074 t.Run(tt.name, func(t *testing.T) { 1075 files := make([]model.File, len(tt.files)) 1076 copy(files, tt.files) 1077 1078 sortFiles(files, file.CoordinatesSorter(tt.layers)) 1079 1080 if d := cmp.Diff(tt.want, files); d != "" { 1081 t.Errorf("sortFiles() mismatch (-want +got):\n%s", d) 1082 } 1083 }) 1084 } 1085 }