Skip to content

test: add fuzz testing infrastructure #7

test: add fuzz testing infrastructure

test: add fuzz testing infrastructure #7

Workflow file for this run

name: Fuzz Tests
on:
schedule:
- cron: "17 3 * * *"
push:
branches:
- main
paths:
- "python/**"
- "controller/**"
- "protocol/**"
- "scripts/fuzz.py"
- ".github/workflows/fuzz.yaml"
workflow_dispatch:
inputs:
fuzz_time:
description: "Fuzz duration per job (e.g. 30m, 2h, 6h)"
default: "30m"
pull_request:
paths:
- "python/**"
- "controller/**"
- "protocol/**"
- "scripts/fuzz.py"
- ".github/workflows/fuzz.yaml"
concurrency:
group: fuzz-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
matrix:
if: github.repository_owner == 'jumpstarter-dev'
runs-on: ubuntu-latest
outputs:
go_targets: ${{ steps.targets.outputs.go_targets }}
fuzz_time: ${{ steps.duration.outputs.time }}
timeout_minutes: ${{ steps.duration.outputs.timeout_minutes }}
steps:
- uses: actions/checkout@v4
- id: targets
run: echo "go_targets=$(python3 scripts/fuzz.py --list-go-targets)" >> "$GITHUB_OUTPUT"
- id: duration
env:
EVENT_NAME: ${{ github.event_name }}
FUZZ_TIME_INPUT: ${{ inputs.fuzz_time }}
run: |
if [ "$EVENT_NAME" = "schedule" ]; then
TIME_VAL="2h"
elif [ "$EVENT_NAME" = "push" ]; then
TIME_VAL="6h"
elif [ "$EVENT_NAME" = "workflow_dispatch" ]; then
if [ -z "$FUZZ_TIME_INPUT" ] || ! echo "$FUZZ_TIME_INPUT" | grep -qE '^([0-9]+h)([0-9]+m)?([0-9]+s)?$|^([0-9]+m)([0-9]+s)?$|^([0-9]+s)$|^[0-9]+$'; then
echo "::error::Invalid fuzz_time format: $FUZZ_TIME_INPUT"
exit 1
fi
TIME_VAL="$FUZZ_TIME_INPUT"
else
TIME_VAL="5m"
fi
echo "time=$TIME_VAL" >> "$GITHUB_OUTPUT"
TOTAL_SECONDS=0
REMAINING="$TIME_VAL"
if echo "$REMAINING" | grep -q 'h'; then
HOURS=$(echo "$REMAINING" | sed 's/h.*//')
REMAINING=$(echo "$REMAINING" | sed 's/[^h]*h//')
TOTAL_SECONDS=$((TOTAL_SECONDS + HOURS * 3600))
fi
if echo "$REMAINING" | grep -q 'm'; then
MINUTES=$(echo "$REMAINING" | sed 's/m.*//')
REMAINING=$(echo "$REMAINING" | sed 's/[^m]*m//')
TOTAL_SECONDS=$((TOTAL_SECONDS + MINUTES * 60))
fi
if echo "$REMAINING" | grep -qE '^[0-9]+s?$'; then
SECS=$(echo "$REMAINING" | sed 's/s//')
if [ -n "$SECS" ]; then
TOTAL_SECONDS=$((TOTAL_SECONDS + SECS))
fi
fi
TIMEOUT_MINUTES=$(( (TOTAL_SECONDS + 59) / 60 + 30 ))
echo "timeout_minutes=$TIMEOUT_MINUTES" >> "$GITHUB_OUTPUT"
fuzz-python:
needs: matrix
runs-on: ubuntu-latest
timeout-minutes: ${{ fromJson(needs.matrix.outputs.timeout_minutes) }}
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- name: Fuzz Python
env:
FUZZ_TIME: ${{ needs.matrix.outputs.fuzz_time }}
run: python3 scripts/fuzz.py --time "$FUZZ_TIME" --python-only
- name: Upload Hypothesis examples
if: failure()
uses: actions/upload-artifact@v4
with:
name: hypothesis-examples
path: python/.hypothesis/
if-no-files-found: ignore
fuzz-go:
needs: matrix
runs-on: ubuntu-latest
timeout-minutes: ${{ fromJson(needs.matrix.outputs.timeout_minutes) }}
strategy:
fail-fast: false
matrix:
target: ${{ fromJson(needs.matrix.outputs.go_targets) }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: controller/go.mod
cache-dependency-path: controller/go.sum
- name: Restore Go fuzz cache
uses: actions/cache@v5
with:
path: ~/.cache/go-build/fuzz
key: go-fuzz-${{ matrix.target.name }}-${{ runner.os }}-${{ hashFiles('controller/**/*_fuzz_test.go') }}
restore-keys: |
go-fuzz-${{ matrix.target.name }}-${{ runner.os }}-
- name: Fuzz ${{ matrix.target.name }}
env:
FUZZ_TIME: ${{ needs.matrix.outputs.fuzz_time }}
GO_TARGET: ${{ matrix.target.name }}
run: python3 scripts/fuzz.py --time "$FUZZ_TIME" --go-target "$GO_TARGET"
- name: Upload crash reproducers
if: failure()
uses: actions/upload-artifact@v4
with:
name: go-crash-${{ matrix.target.name }}
path: controller/**/testdata/fuzz/${{ matrix.target.name }}/
if-no-files-found: ignore