@@ -25,6 +25,7 @@ import (
2525 "encoding/json"
2626 "fmt"
2727 "os"
28+ "path/filepath"
2829 "reflect"
2930 "regexp"
3031 "sort"
@@ -34,19 +35,6 @@ import (
3435 "github.com/package-url/packageurl-go"
3536)
3637
37- type TestFixture struct {
38- Description string `json:"description"`
39- Purl string `json:"purl"`
40- CanonicalPurl string `json:"canonical_purl"`
41- PackageType string `json:"type"`
42- Namespace string `json:"namespace"`
43- Name string `json:"name"`
44- Version string `json:"version"`
45- QualifierMap OrderedMap `json:"qualifiers"`
46- Subpath string `json:"subpath"`
47- IsInvalid bool `json:"is_invalid"`
48- }
49-
5038// OrderedMap is used to store the TestFixture.QualifierMap, to ensure that the
5139// declaration order of qualifiers is preserved.
5240type OrderedMap struct {
@@ -107,9 +95,18 @@ func (m *OrderedMap) UnmarshalJSON(bytes []byte) error {
10795 }
10896}
10997
110- // Qualifiers converts the TestFixture.QualifierMap field to an object of type
98+ type ComponentData struct {
99+ PackageType string `json:"type"`
100+ Namespace string `json:"namespace"`
101+ Name string `json:"name"`
102+ Version string `json:"version"`
103+ QualifierMap OrderedMap `json:"qualifiers"`
104+ Subpath string `json:"subpath"`
105+ }
106+
107+ // Qualifiers converts the ComponentData.QualifierMap field to an object of type
111108// packageurl.Qualifiers.
112- func (t TestFixture ) Qualifiers () packageurl.Qualifiers {
109+ func (t ComponentData ) Qualifiers () packageurl.Qualifiers {
113110 q := packageurl.Qualifiers {}
114111
115112 for _ , key := range t .QualifierMap .OrderedKeys {
@@ -119,150 +116,198 @@ func (t TestFixture) Qualifiers() packageurl.Qualifiers {
119116 return q
120117}
121118
122- // TestFromStringExamples verifies that parsing example strings produce expected
123- // results.
124- func TestFromStringExamples (t * testing.T ) {
125- // Read the json file
126- data , err := os .ReadFile ("testdata/test-suite-data.json" )
127- if err != nil {
128- t .Fatal (err )
119+ type ComponentsOrPurl struct {
120+ Purl * string
121+ PurlComponent * ComponentData
122+ }
123+
124+ func (cop * ComponentsOrPurl ) UnmarshalJSON (data []byte ) error {
125+ // Try string first
126+ var s string
127+ if err := json .Unmarshal (data , & s ); err == nil {
128+ cop .Purl = & s
129+ return nil
130+ }
131+
132+ var comp ComponentData
133+ if err := json .Unmarshal (data , & comp ); err == nil {
134+ cop .PurlComponent = & comp
135+ return nil
129136 }
130- // Load the json file contents into a structure
131- testData := []TestFixture {}
132- err = json .Unmarshal (data , & testData )
137+
138+ return fmt .Errorf ("ComponentsOrPurl: data is neither a string nor PURL component" )
139+ }
140+
141+ type TestFixture struct {
142+ Description string `json:"description"`
143+ TestGroup string `json:"test_group"`
144+ TestType string `json:"test_type"`
145+ Input ComponentsOrPurl `json:"input"`
146+ ExpectedFailure bool `json:"expected_failure"`
147+ ExpectedOutput ComponentsOrPurl `json:"expected_output"`
148+ ExpectedFailureMsg * string `json:"expected_failure_reason"`
149+ }
150+
151+ type TestSuite struct {
152+ Schema string `json:"$schema"`
153+ Tests []TestFixture `json:"tests"`
154+ }
155+
156+ func readJSONFilesFromDir (dirPath string ) ([][]byte , error ) {
157+ var result [][]byte
158+
159+ entries , err := os .ReadDir (dirPath )
133160 if err != nil {
134- t . Fatal ( err )
161+ return nil , fmt . Errorf ( "reading dir %s: %w" , dirPath , err )
135162 }
136163
137- // Use FromString on each item in the test set
138- for _ , tc := range testData {
139- // Should parse without issue
140- p , err := packageurl .FromString (tc .Purl )
141- if tc .IsInvalid == false {
142- if err != nil {
143- t .Logf ("%s failed: %s" , tc .Description , err )
144- t .Fail ()
145- }
146- // verify parsing
147- if p .Type != tc .PackageType {
148- t .Logf ("%s: incorrect package type: wanted: '%s', got '%s'" , tc .Description , tc .PackageType , p .Type )
149- t .Fail ()
150- }
151- if p .Namespace != tc .Namespace {
152- t .Logf ("%s: incorrect namespace: wanted: '%s', got '%s'" , tc .Description , tc .Namespace , p .Namespace )
153- t .Fail ()
154- }
155- if p .Name != tc .Name {
156- t .Logf ("%s: incorrect name: wanted: '%s', got '%s'" , tc .Description , tc .Name , p .Name )
157- t .Fail ()
158- }
159- if p .Version != tc .Version {
160- t .Logf ("%s: incorrect version: wanted: '%s', got '%s'" , tc .Description , tc .Version , p .Version )
161- t .Fail ()
162- }
163- want := tc .Qualifiers ()
164- sort .Slice (want , func (i , j int ) bool {
165- return want [i ].Key < want [j ].Key
166- })
167- got := p .Qualifiers
168- sort .Slice (got , func (i , j int ) bool {
169- return got [i ].Key < got [j ].Key
170- })
171- if ! reflect .DeepEqual (want , got ) {
172- t .Logf ("%s: incorrect qualifiers: wanted: '%#v', got '%#v'" , tc .Description , want , p .Qualifiers )
173- t .Fail ()
174- }
164+ for _ , entry := range entries {
165+ if entry .IsDir () || filepath .Ext (entry .Name ()) != ".json" {
166+ continue
167+ }
168+
169+ fullPath := filepath .Join (dirPath , entry .Name ())
170+ data , err := os .ReadFile (fullPath )
171+ if err != nil {
172+ return nil , fmt .Errorf ("reading file %s: %w" , fullPath , err )
173+ }
174+
175+ result = append (result , data )
176+ }
175177
176- if p .Subpath != tc .Subpath {
177- t .Logf ("%s: incorrect subpath: wanted: '%s', got '%s'" , tc .Description , tc .Subpath , p .Subpath )
178+ return result , nil
179+ }
180+
181+ func roundTripTest (tc TestFixture , t * testing.T ) {
182+ p , err := packageurl .FromString (* tc .Input .Purl )
183+ if tc .ExpectedFailure == false {
184+ if err != nil {
185+ t .Logf ("%s failed: %s" , tc .Description , err )
186+ t .Fail ()
187+ }
188+
189+ if tc .ExpectedOutput .Purl != nil {
190+ if * tc .ExpectedOutput .Purl != p .String () {
191+ t .Logf ("%s: '%s' test failed: wanted: '%s', got '%s'" , tc .Description , tc .TestType , * tc .ExpectedOutput .Purl , p .String ())
178192 t .Fail ()
179193 }
180194 } else {
181- // Invalid cases
182- if err == nil {
183- t .Logf ("%s did not fail and returned %#v" , tc .Description , p )
184- t .Fail ()
185- }
195+ t .Logf ("%s: expected output nil: '%s'" , tc .Description , * tc .ExpectedOutput .Purl )
196+ t .Fail ()
197+ }
198+
199+ } else {
200+ if err == nil {
201+ t .Logf ("%s did not fail and returned %#v" , tc .Description , p )
202+ t .Fail ()
186203 }
204+
187205 }
188206}
189207
190- // TestToStringExamples verifies that the resulting package urls created match
191- // the expected format.
192- func TestToStringExamples (t * testing.T ) {
193- // Read the json file
194- data , err := os .ReadFile ("testdata/test-suite-data.json" )
195- if err != nil {
196- t .Fatal (err )
197- }
198- // Load the json file contents into a structure
199- var testData []TestFixture
200- err = json .Unmarshal (data , & testData )
201- if err != nil {
202- t .Fatal (err )
203- }
204- // Use ToString on each item
205- for _ , tc := range testData {
206- // Skip invalid items
207- if tc .IsInvalid == true {
208- continue
208+ func parseTest (tc TestFixture , t * testing.T ) {
209+ p , err := packageurl .FromString (* tc .Input .Purl )
210+ if tc .ExpectedFailure == false {
211+ if err != nil {
212+ t .Logf ("%s failed: %s" , tc .Description , err )
213+ t .Fail ()
214+ }
215+ // verify parsing
216+ expected := tc .ExpectedOutput .PurlComponent
217+ if p .Type != expected .PackageType {
218+ t .Logf ("%s: incorrect package type: wanted: '%s', got '%s'" , tc .Description , expected .PackageType , p .Type )
219+ t .Fail ()
220+ }
221+ if p .Namespace != expected .Namespace {
222+ t .Logf ("%s: incorrect namespace: wanted: '%s', got '%s'" , tc .Description , expected .Namespace , p .Namespace )
223+ t .Fail ()
224+ }
225+ if p .Name != expected .Name {
226+ t .Logf ("%s: incorrect name: wanted: '%s', got '%s'" , tc .Description , expected .Name , p .Name )
227+ t .Fail ()
228+ }
229+ if p .Version != expected .Version {
230+ t .Logf ("%s: incorrect version: wanted: '%s', got '%s'" , tc .Description , expected .Version , p .Version )
231+ t .Fail ()
232+ }
233+ want := expected .Qualifiers ()
234+ sort .Slice (want , func (i , j int ) bool {
235+ return want [i ].Key < want [j ].Key
236+ })
237+ got := p .Qualifiers
238+ sort .Slice (got , func (i , j int ) bool {
239+ return got [i ].Key < got [j ].Key
240+ })
241+ if ! reflect .DeepEqual (want , got ) {
242+ t .Logf ("%s: incorrect qualifiers: wanted: '%#v', got '%#v'" , tc .Description , want , p .Qualifiers )
243+ t .Fail ()
244+ }
245+
246+ if p .Subpath != expected .Subpath {
247+ t .Logf ("%s: incorrect subpath: wanted: '%s', got '%s'" , tc .Description , expected .Subpath , p .Subpath )
248+ t .Fail ()
209249 }
210- instance := packageurl .NewPackageURL (
211- tc .PackageType , tc .Namespace , tc .Name , tc .Version ,
212- // Use QualifiersFromMap so that the qualifiers have a defined order, which is needed for string comparisons
213- packageurl .QualifiersFromMap (tc .Qualifiers ().Map ()), tc .Subpath )
214- result := instance .ToString ()
215-
216- // NOTE: We create a purl with ToString and then load into a PackageURL
217- // because qualifiers may not be in any order. By reparsing back
218- // we can ensure the data transfers between string and instance form.
219- canonical , _ := packageurl .FromString (tc .CanonicalPurl )
220- toTest , _ := packageurl .FromString (result )
221- // If the two results don't equal then the ToString failed
222- if ! reflect .DeepEqual (toTest , canonical ) {
223- t .Logf ("%s failed: %s != %s" , tc .Description , result , tc .CanonicalPurl )
250+ } else {
251+ // Invalid cases
252+ if err == nil {
253+ t .Logf ("%s did not fail and returned %#v" , tc .Description , p )
224254 t .Fail ()
225255 }
226256 }
257+
227258}
228259
229- // TestStringer verifies that the Stringer implementation produces results
230- // equivalent with the ToString method.
231- func TestStringer (t * testing.T ) {
232- // Read the json file
233- data , err := os .ReadFile ("testdata/test-suite-data.json" )
234- if err != nil {
235- t .Fatal (err )
260+ func buildTest (tc TestFixture , t * testing.T ) {
261+ input := tc .Input .PurlComponent
262+ instance := packageurl .NewPackageURL (
263+ input .PackageType , input .Namespace , input .Name , input .Version ,
264+ // Use QualifiersFromMap so that the qualifiers have a defined order, which is needed for string comparisons
265+ packageurl .QualifiersFromMap (input .Qualifiers ().Map ()), input .Subpath )
266+ result := instance .ToString ()
267+ canonicalExpectedPurl := tc .ExpectedOutput .Purl
268+
269+ if tc .ExpectedFailure == false {
270+ if result != * canonicalExpectedPurl {
271+ t .Logf ("%s: '%s' test failed: wanted: '%s', got '%s'" , tc .Description , tc .TestType , * canonicalExpectedPurl , result )
272+ t .Fail ()
273+ }
274+ } else {
275+ t .Logf ("%s did not fail and returned %#v" , tc .Description , instance )
276+ t .Fail ()
236277 }
237- // Load the json file contents into a structure
238- var testData []TestFixture
239- err = json .Unmarshal (data , & testData )
278+
279+ }
280+
281+ func TestPurlSpecFixtures (t * testing.T ) {
282+ testFiles , err := readJSONFilesFromDir ("testdata/purl-spec/tests/types/" )
240283 if err != nil {
241284 t .Fatal (err )
242285 }
243- // Use ToString on each item
244- for _ , tc := range testData {
245- // Skip invalid items
246- if tc .IsInvalid == true {
247- continue
248- }
249- purlPtr := packageurl .NewPackageURL (
250- tc .PackageType , tc .Namespace , tc .Name ,
251- tc .Version , tc .Qualifiers (), tc .Subpath )
252- purlValue := * purlPtr
253-
254- // Verify that the Stringer implementation returns a result
255- // equivalent to ToString().
256- if purlPtr .ToString () != purlPtr .String () {
257- t .Logf ("%s failed: Stringer implementation differs from ToString: %s != %s" , tc .Description , purlPtr .String (), purlPtr .ToString ())
258- t .Fail ()
286+
287+ for _ , data := range testFiles {
288+ var suite TestSuite
289+ err := json .Unmarshal (data , & suite )
290+ if err != nil {
291+ t .Fatal (err )
259292 }
260293
261- // Verify that the %s format modifier works for values.
262- fmtStr := purlValue .String ()
263- if fmtStr != purlPtr .String () {
264- t .Logf ("%s failed: %%s format modifier does not work on values: %s != %s" , tc .Description , fmtStr , purlPtr .ToString ())
265- t .Fail ()
294+ for _ , tc := range suite .Tests {
295+ t .Run (tc .TestType , func (t * testing.T ) {
296+ testType := tc .TestType
297+
298+ switch testType {
299+ case "roundtrip" :
300+ roundTripTest (tc , t )
301+ case "parse" :
302+ parseTest (tc , t )
303+ case "build" :
304+ buildTest (tc , t )
305+ default :
306+ t .Fatalf ("Unsupported test type: %s" , testType )
307+ }
308+
309+ })
310+
266311 }
267312 }
268313}
0 commit comments