diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..62f3e6c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + commit-message: + prefix: ci + cooldown: + default-days: 7 + - package-ecosystem: gitsubmodule + directory: / + schedule: + interval: weekly + commit-message: + prefix: test + cooldown: + default-days: 7 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1feecff..59bc6a7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,22 +2,32 @@ name: CI on: push: + branches: + - main pull_request: -env: - GO111MODULE: 'on' +permissions: + contents: read + +# Superseded pull request runs are worthless; main runs still upload coverage. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: lint: name: Lint runs-on: ubuntu-latest steps: - - name: Check out code - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Go - uses: actions/setup-go@v5 - - name: Lint Go Code - uses: golangci/golangci-lint-action@v6 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: stable + cache: false # golangci-lint-action manages its own cache + - name: Lint Go code + uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 test: name: Test @@ -28,15 +38,17 @@ jobs: go-version: ['stable', 'oldstable'] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: true - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: ${{ matrix.go-version }} - name: Test - run: go test -race ${{ matrix.go-version == 'stable' && '-covermode atomic -coverprofile=profile.cov' || ''}} + run: go test -race ${{ matrix.go-version == 'stable' && '-covermode atomic -coverprofile=profile.cov' || '' }} - name: Upload coverage results if: matrix.go-version == 'stable' - uses: shogo82148/actions-goveralls@v1 + uses: shogo82148/actions-goveralls@77a1912dca42260ee3e97f61bd13ea7ef40baa93 # v1.11.2 with: path-to-profile: profile.cov diff --git a/.github/workflows/dependabot.yaml b/.github/workflows/dependabot.yaml new file mode 100644 index 0000000..ecb39ef --- /dev/null +++ b/.github/workflows/dependabot.yaml @@ -0,0 +1,40 @@ +name: Dependabot Auto-Merge + +on: + pull_request_target: + branches: + - main + +permissions: {} + +jobs: + auto-merge: + runs-on: ubuntu-latest + environment: release + if: github.event.pull_request.user.login == 'dependabot[bot]' + steps: + # The default GITHUB_TOKEN lacks the `workflows` scope and is refused on + # PRs that touch .github/workflows; use the release app token instead. + - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + id: app-token + with: + app-id: ${{ vars.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + permission-workflows: write + - name: Fetch Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 + with: + github-token: ${{ steps.app-token.outputs.token }} + # Only minor and patch GitHub Actions bumps auto-merge; majors and the + # test suite submodule wait for a human review. + - name: Auto-merge minor and patch GitHub Actions updates + if: steps.metadata.outputs.package-ecosystem == 'github_actions' && steps.metadata.outputs.update-type != 'version-update:semver-major' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + PR_URL: ${{ github.event.pull_request.html_url }} + run: | + gh pr review --approve "${PR_URL}" + gh pr merge --auto --squash "${PR_URL}" diff --git a/dictionary.go b/dictionary.go index 7e73bcd..9f6580c 100644 --- a/dictionary.go +++ b/dictionary.go @@ -8,8 +8,8 @@ import ( // Dictionary is an ordered map of name-value pairs. // See https://httpwg.org/specs/rfc9651.html#dictionary // Values can be: -// * Item (Section 3.3.) -// * Inner List (Section 3.1.1.) +// - Item (Section 3.3.) +// - Inner List (Section 3.1.1.) type Dictionary struct { names []string values map[string]Member @@ -104,7 +104,7 @@ func (d *Dictionary) marshalSFV(b *strings.Builder) error { // https://httpwg.org/specs/rfc9651.html#parse-dictionary. func UnmarshalDictionary(v []string) (*Dictionary, error) { s := &scanner{ - data: strings.Join(v, ","), + data: strings.Join(v, ", "), } s.scanWhileSp() diff --git a/httpwg_test.go b/httpwg_test.go index 0b9f242..1673cd8 100644 --- a/httpwg_test.go +++ b/httpwg_test.go @@ -141,25 +141,68 @@ func valToDictionary(e interface{}) *Dictionary { return d } -func TestOfficialTestSuiteParsing(t *testing.T) { - const dir = "structured-field-tests/" - f, _ := os.Open(dir) - files, _ := f.Readdir(-1) +// listTestFiles returns the JSON test suite files in dir. +func listTestFiles(tb testing.TB, dir string) []string { + tb.Helper() - for _, fi := range files { - n := fi.Name() - if !strings.HasSuffix(n, ".json") { - continue + f, err := os.Open(dir) + if err != nil { + tb.Fatalf("%s: %s (is the structured-field-tests submodule checked out?)", dir, err) + } + + defer func() { _ = f.Close() }() + + entries, err := f.Readdir(-1) + if err != nil { + tb.Fatalf("%s: %s", dir, err) + } + + var names []string + + for _, fi := range entries { + if strings.HasSuffix(fi.Name(), ".json") { + names = append(names, fi.Name()) } + } + + if len(names) == 0 { + tb.Fatalf("%s: no JSON test file found (is the structured-field-tests submodule checked out?)", dir) + } - file, _ := os.Open(dir + n) - dec := json.NewDecoder(file) - dec.UseNumber() + return names +} - var tests []test - _ = dec.Decode(&tests) +// loadTests decodes the test cases contained in the given test suite file. +func loadTests(tb testing.TB, path string) []test { + tb.Helper() - for _, te := range tests { + file, err := os.Open(path) + if err != nil { + tb.Fatalf("%s: %s (is the structured-field-tests submodule checked out?)", path, err) + } + + defer func() { _ = file.Close() }() + + dec := json.NewDecoder(file) + dec.UseNumber() + + var tests []test + if err := dec.Decode(&tests); err != nil { + tb.Fatalf("%s: %s", path, err) + } + + if len(tests) == 0 { + tb.Fatalf("%s: no test case found", path) + } + + return tests +} + +func TestOfficialTestSuiteParsing(t *testing.T) { + const dir = "structured-field-tests/" + + for _, n := range listTestFiles(t, dir) { + for _, te := range loadTests(t, dir+n) { t.Run(n+"/"+te.Name, func(t *testing.T) { var ( expected, got StructuredFieldValue @@ -201,11 +244,7 @@ func TestOfficialTestSuiteParsing(t *testing.T) { } func BenchmarkParsingOfficialExamples(b *testing.B) { - file, _ := os.Open("structured-field-tests/examples.json") - dec := json.NewDecoder(file) - - var tests []test - _ = dec.Decode(&tests) + tests := loadTests(b, "structured-field-tests/examples.json") for n := 0; n < b.N; n++ { for _, te := range tests { @@ -222,12 +261,7 @@ func BenchmarkParsingOfficialExamples(b *testing.B) { } func BenchmarkSerializingOfficialExamples(b *testing.B) { - file, _ := os.Open("structured-field-tests/examples.json") - dec := json.NewDecoder(file) - dec.UseNumber() - - var tests []test - _ = dec.Decode(&tests) + tests := loadTests(b, "structured-field-tests/examples.json") var sfv []StructuredFieldValue @@ -258,23 +292,8 @@ func TestOfficialTestSuiteSerialization(t *testing.T) { const dir = "structured-field-tests/serialisation-tests/" - f, _ := os.Open(dir) - files, _ := f.Readdir(-1) - - for _, fi := range files { - n := fi.Name() - if !strings.HasSuffix(n, ".json") { - continue - } - - file, _ := os.Open(dir + n) - dec := json.NewDecoder(file) - dec.UseNumber() - - var tests []test - _ = dec.Decode(&tests) - - for _, te := range tests { + for _, n := range listTestFiles(t, dir) { + for _, te := range loadTests(t, dir+n) { var sfv StructuredFieldValue switch te.HeaderType { diff --git a/item.go b/item.go index 000c999..eb8b9ed 100644 --- a/item.go +++ b/item.go @@ -39,7 +39,7 @@ func (i Item) marshalSFV(b *strings.Builder) error { // https://httpwg.org/specs/rfc9651.html#parse-item. func UnmarshalItem(v []string) (Item, error) { s := &scanner{ - data: strings.Join(v, ","), + data: strings.Join(v, ", "), } s.scanWhileSp() diff --git a/item_test.go b/item_test.go index cf0537a..c17f1d6 100644 --- a/item_test.go +++ b/item_test.go @@ -79,7 +79,7 @@ func TestUnmarshalItem(t *testing.T) { }{ {[]string{"?1;foo;*bar=tok"}, i1, false}, {[]string{" ?1;foo;*bar=tok "}, i1, false}, - {[]string{`"foo`, `bar"`}, NewItem("foo,bar"), false}, + {[]string{`"foo`, `bar"`}, NewItem("foo, bar"), false}, {[]string{"é", ""}, Item{}, true}, {[]string{"tok;é"}, Item{}, true}, {[]string{" ?1;foo;*bar=tok é"}, Item{}, true}, diff --git a/list.go b/list.go index 7c20b0d..cc8a62c 100644 --- a/list.go +++ b/list.go @@ -36,7 +36,7 @@ func (l List) marshalSFV(b *strings.Builder) error { // https://httpwg.org/specs/rfc9651.html#parse-list. func UnmarshalList(v []string) (List, error) { s := &scanner{ - data: strings.Join(v, ","), + data: strings.Join(v, ", "), } s.scanWhileSp() diff --git a/structured-field-tests b/structured-field-tests index 7970aff..1e280c3 160000 --- a/structured-field-tests +++ b/structured-field-tests @@ -1 +1 @@ -Subproject commit 7970aff32a4e73452beedde861f4eb52ed4243b0 +Subproject commit 1e280c3ed9ffe0ca5fdb1d97219dddc389007677