9342ce0eff
Add FuzzParseBytes and FuzzSanitizeYAMLEscapes in internal/model/fuzz_test.go. Both targets run as regular seed-based tests in CI (go test ./...) and can be run continuously via `task fuzz` (default 30 s per target; FUZZ_TIME=60s to extend). Fuzz corpus failures are saved to testdata/fuzz/ for regression. Add cliff.toml configuring git-cliff to generate Keep-a-Changelog-compatible release notes from Conventional Commits. New tasks: `task changelog` (regenerate full CHANGELOG.md) and `task changelog-next` (preview unreleased entries without writing). Requires git-cliff (brew/cargo install git-cliff). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
78 lines
2.9 KiB
Go
78 lines
2.9 KiB
Go
package model
|
|
|
|
import "testing"
|
|
|
|
// FuzzParseBytes ensures the YAML parser never panics on arbitrary input and
|
|
// that successful parses return a structurally sound Pipeline.
|
|
// Run with: go test -fuzz=FuzzParseBytes ./internal/model/
|
|
// Found failures are saved to testdata/fuzz/FuzzParseBytes/.
|
|
func FuzzParseBytes(f *testing.F) {
|
|
// Seed corpus: representative inputs covering the main code paths in
|
|
// ParseBytes, including the sanitizeYAMLEscapes pre-processing step.
|
|
seeds := [][]byte{
|
|
{},
|
|
[]byte("null"),
|
|
[]byte("stages: [build]\nbuild-job:\n stage: build\n script: echo ok\n"),
|
|
[]byte(".base:\n script: [make]\nchild:\n extends: .base\n stage: test\n"),
|
|
[]byte("stages: [a, b]\njob-a:\n stage: a\n script: run\njob-b:\n stage: b\n needs: [job-a]\n script: run\n"),
|
|
[]byte("*undefined_anchor"),
|
|
[]byte("- item1\n- item2\n"),
|
|
[]byte("my-job: \"just a string\"\n"),
|
|
[]byte("my-job:\n stage: [build, test]\n"),
|
|
[]byte("# glint: ignore GL007\nlegacy:\n only: [main]\n script: ok\n"),
|
|
[]byte("workflow:\n rules:\n - if: '$CI_COMMIT_BRANCH == \"main\"'\n when: always\njob:\n script: echo ok\n"),
|
|
[]byte("include:\n - local: other.yml\njob:\n script: echo ok\n"),
|
|
[]byte("job:\n script: echo ok\n when: on_failure\n rules:\n - if: '$VAR =~ /^us\\//'\n"),
|
|
[]byte("job:\n stage: test\n image:\n name: golang:1.21\n entrypoint: ['']\n parallel:\n matrix:\n - PLATFORM: [linux, darwin]\n script: go build\n"),
|
|
[]byte("default:\n retry: 2\n timeout: 1h30m\nvariables:\n ENV: production\nstages: [build, test, deploy]\n"),
|
|
[]byte("&anchor\n script: [echo ok]\njob:\n <<: *anchor\n stage: build\n"),
|
|
}
|
|
for _, s := range seeds {
|
|
f.Add(s)
|
|
}
|
|
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
|
p, err := ParseBytes(data)
|
|
if err != nil {
|
|
return // errors are acceptable; panics are not
|
|
}
|
|
if p == nil {
|
|
t.Fatal("ParseBytes returned nil pipeline with nil error")
|
|
}
|
|
for name := range p.Jobs {
|
|
if name == "" {
|
|
t.Fatal("ParseBytes produced a job with an empty name")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzSanitizeYAMLEscapes ensures the escape sanitizer never panics and never
|
|
// produces output shorter than its input (it can only expand \/ to \\/).
|
|
func FuzzSanitizeYAMLEscapes(f *testing.F) {
|
|
seeds := [][]byte{
|
|
{},
|
|
[]byte("stage: build"),
|
|
[]byte(`if: "$CI_BRANCH =~ /^us\//"`),
|
|
[]byte(`"pattern: /^us\//"`),
|
|
[]byte(`'single quoted \/ unchanged'`),
|
|
[]byte(`"\n\t\r"`),
|
|
[]byte(`"nested \"quote\" inside"`),
|
|
[]byte(`'it''s fine'`),
|
|
[]byte(`"unclosed`),
|
|
{'"', '\\'}, // double-quoted string ending with a lone backslash
|
|
{'"', '\\', '/'}, // the exact sequence being rewritten
|
|
}
|
|
for _, s := range seeds {
|
|
f.Add(s)
|
|
}
|
|
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
|
out := sanitizeYAMLEscapes(data)
|
|
if len(out) < len(data) {
|
|
t.Fatalf("sanitizeYAMLEscapes shrank output: input len=%d output len=%d\ninput: %q",
|
|
len(data), len(out), data)
|
|
}
|
|
})
|
|
}
|