Skip to content

Commit fd0a06d

Browse files
committed
Run tests using reference data from purl-spec
Signed-off-by: Keshav Priyadarshi <[email protected]>
1 parent cb1f648 commit fd0a06d

File tree

3 files changed

+184
-139
lines changed

3 files changed

+184
-139
lines changed

.github/workflows/test.yaml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ jobs:
1414
go-version: ${{ matrix.go-version }}
1515
- name: Checkout code
1616
uses: actions/checkout@v4
17-
- name: Download test data
18-
# TODO(@shibumi): Remove pinned version and reset to master, once the failing npm test-cases got fixed.
19-
run: curl -L https://raw.githubusercontent.com/package-url/purl-spec/0dd92f26f8bb11956ffdf5e8acfcee71e8560407/test-suite-data.json -o testdata/test-suite-data.json
17+
with:
18+
submodules: true
2019
- name: Test go fmt
2120
run: test -z $(go fmt ./...)
2221
- name: Golangci-lint

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
.PHONY: test clean lint
22

33
test:
4-
curl -Ls https://raw.githubusercontent.com/package-url/purl-spec/master/test-suite-data.json -o testdata/test-suite-data.json
4+
git submodule update --init
5+
# git submodule update --remote
56
go test -v -cover ./...
67

78
fuzz:

packageurl_test.go

Lines changed: 180 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
5240
type 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

Comments
 (0)