diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 00000000..ab9905d7 --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,78 @@ +name: Fuzz Tests + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + fuzz: + name: Fuzz (${{ matrix.name }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - pkg: ./interp/builtins/tests/head/ + name: head + - pkg: ./interp/builtins/tests/cat/ + name: cat + - pkg: ./interp/builtins/tests/wc/ + name: wc + - pkg: ./interp/builtins/tests/tail/ + name: tail + - pkg: ./interp/builtins/tests/grep/ + name: grep + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + with: + go-version-file: .go-version + + # Restore corpus from previous runs + - name: Restore fuzz corpus + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + interp/builtins/tests/${{ matrix.name }}/testdata/fuzz/ + key: fuzz-corpus-${{ matrix.name }}-${{ github.sha }} + restore-keys: | + fuzz-corpus-${{ matrix.name }}- + + # Run seed corpus as normal tests (fast, deterministic) + - name: Run fuzz seed corpus + run: | + # Find all Fuzz* functions in the package (excluding differential ones that need RSHELL_BASH_TEST) + FUZZ_FUNCS=$(grep -r '^func Fuzz' ${{ matrix.pkg }} 2>/dev/null | grep -v 'Differential' | sed 's/.*func \(Fuzz[^(]*\).*/\1/' | sort -u | tr '\n' '|' | sed 's/|$//') + if [ -n "$FUZZ_FUNCS" ]; then + go test -run "^(${FUZZ_FUNCS})$" -fuzztime=0s ${{ matrix.pkg }} -timeout 120s + else + echo "No non-differential fuzz functions found in ${{ matrix.pkg }}, skipping" + fi + + # Run actual fuzzing for a short duration + - name: Fuzz (${{ matrix.name }}) + run: | + FUZZ_FUNCS=$(grep -r '^func Fuzz' ${{ matrix.pkg }} 2>/dev/null | grep -v 'Differential' | sed 's/.*func \(Fuzz[^(]*\).*/\1/' | sort -u) + if [ -z "$FUZZ_FUNCS" ]; then + echo "No fuzz targets found in ${{ matrix.pkg }}, skipping" + exit 0 + fi + FAILED=0 + for FUNC in $FUZZ_FUNCS; do + echo "Fuzzing $FUNC..." + go test -fuzz="^${FUNC}$" -fuzztime=30s ${{ matrix.pkg }} -timeout 60s || FAILED=1 + done + exit $FAILED + + # Save corpus + - name: Save fuzz corpus + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + if: always() + with: + path: | + interp/builtins/tests/${{ matrix.name }}/testdata/fuzz/ + key: fuzz-corpus-${{ matrix.name }}-${{ github.sha }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2dec643f..57ec346c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,8 @@ jobs: go-version-file: .go-version - name: Run tests with race detector run: go test -race -v ./... + - name: Run fuzz seed corpus (regression test) + run: go test -run '^Fuzz' ./interp/builtins/... -timeout 120s gofmt: name: gofmt diff --git a/.gitignore b/.gitignore index 3a8c62d8..b102bc82 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ /rshell .DS_Store + +# Fuzz corpus: keep checked in for regression testing. +# Uncomment the line below if corpus grows too large: +# interp/builtins/tests/*/testdata/fuzz/*/corpus-* diff --git a/interp/builtins/tests/cat/testdata/fuzz/.gitkeep b/interp/builtins/tests/cat/testdata/fuzz/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/interp/builtins/tests/grep/testdata/fuzz/.gitkeep b/interp/builtins/tests/grep/testdata/fuzz/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/interp/builtins/tests/head/testdata/fuzz/.gitkeep b/interp/builtins/tests/head/testdata/fuzz/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/interp/builtins/tests/tail/testdata/fuzz/.gitkeep b/interp/builtins/tests/tail/testdata/fuzz/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/interp/builtins/tests/wc/testdata/fuzz/.gitkeep b/interp/builtins/tests/wc/testdata/fuzz/.gitkeep new file mode 100644 index 00000000..e69de29b