github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/cmd/syft/internal/test/integration/encode_decode_cycle_test.go (about) 1 package integration 2 3 import ( 4 "bytes" 5 "regexp" 6 "testing" 7 8 "github.com/google/go-cmp/cmp" 9 "github.com/sergi/go-diff/diffmatchpatch" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 13 "github.com/anchore/syft/cmd/syft/internal/options" 14 "github.com/anchore/syft/syft/format" 15 "github.com/anchore/syft/syft/format/cyclonedxjson" 16 "github.com/anchore/syft/syft/format/cyclonedxxml" 17 "github.com/anchore/syft/syft/format/syftjson" 18 "github.com/anchore/syft/syft/source" 19 ) 20 21 // TestEncodeDecodeEncodeCycleComparison is testing for differences in how SBOM documents get encoded on multiple cycles. 22 // By encoding and decoding the sbom we can compare the differences between the set of resulting objects. However, 23 // this requires specific comparisons being done, and select redactions/omissions being made. Additionally, there are 24 // already unit tests on each format encoder-decoder for properly functioning comparisons in depth, so there is no need 25 // to do an object-to-object comparison. For this reason this test focuses on a bytes-to-bytes comparison after an 26 // encode-decode-encode loop which will detect lossy behavior in both directions. 27 func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { 28 // use second image for relationships 29 images := []string{ 30 "image-pkg-coverage", 31 "image-owning-package", 32 } 33 tests := []struct { 34 name string 35 redactor func(in []byte) []byte 36 json bool 37 }{ 38 { 39 name: syftjson.ID.String(), 40 redactor: func(in []byte) []byte { 41 // no redactions necessary 42 return in 43 }, 44 json: true, 45 }, 46 { 47 name: cyclonedxjson.ID.String(), 48 redactor: func(in []byte) []byte { 49 // unstable values 50 in = regexp.MustCompile(`"(timestamp|serialNumber|bom-ref|ref)":\s*"(\n|[^"])+"`).ReplaceAll(in, []byte(`"$1": "redacted"`)) 51 in = regexp.MustCompile(`"(dependsOn)":\s*\[(?:\s|[^]])+]`).ReplaceAll(in, []byte(`"$1": []`)) 52 return in 53 }, 54 json: true, 55 }, 56 { 57 name: cyclonedxxml.ID.String(), 58 redactor: func(in []byte) []byte { 59 // unstable values 60 in = regexp.MustCompile(`(serialNumber|bom-ref|ref)="[^"]+"`).ReplaceAll(in, []byte{}) 61 in = regexp.MustCompile(`<timestamp>[^<]+</timestamp>`).ReplaceAll(in, []byte{}) 62 63 return in 64 }, 65 }, 66 } 67 68 opts := options.DefaultOutput() 69 require.NoError(t, opts.PostLoad()) 70 encoderList, err := opts.Encoders() 71 require.NoError(t, err) 72 73 encoders := format.NewEncoderCollection(encoderList...) 74 decoders := format.NewDecoderCollection(format.Decoders()...) 75 76 for _, test := range tests { 77 t.Run(test.name, func(t *testing.T) { 78 for _, image := range images { 79 originalSBOM, _ := catalogFixtureImage(t, image, source.SquashedScope) 80 81 f := encoders.GetByString(test.name) 82 require.NotNil(t, f) 83 84 var buff1 bytes.Buffer 85 err := f.Encode(&buff1, originalSBOM) 86 require.NoError(t, err) 87 88 newSBOM, formatID, formatVersion, err := decoders.Decode(bytes.NewReader(buff1.Bytes())) 89 require.NoError(t, err) 90 require.Equal(t, f.ID(), formatID) 91 require.Equal(t, f.Version(), formatVersion) 92 93 var buff2 bytes.Buffer 94 err = f.Encode(&buff2, *newSBOM) 95 require.NoError(t, err) 96 97 by1 := buff1.Bytes() 98 by2 := buff2.Bytes() 99 if test.redactor != nil { 100 by1 = test.redactor(by1) 101 by2 = test.redactor(by2) 102 } 103 104 if test.json { 105 s1 := string(by1) 106 s2 := string(by2) 107 if diff := cmp.Diff(s1, s2); diff != "" { 108 t.Errorf("Encode/Decode mismatch (-want +got) [image %q]:\n%s", image, diff) 109 } 110 } else if !assert.True(t, bytes.Equal(by1, by2)) { 111 dmp := diffmatchpatch.New() 112 diffs := dmp.DiffMain(string(by1), string(by2), true) 113 t.Errorf("diff: %s", dmp.DiffPrettyText(diffs)) 114 } 115 } 116 }) 117 } 118 }