Skip to content

Coverage guided mutation fuzzing and mutation agnostic tests #44

Coverage guided mutation fuzzing and mutation agnostic tests

Coverage guided mutation fuzzing and mutation agnostic tests #44

Workflow file for this run

name: VDOM Fuzz
on:
push:
branches:
- main
paths:
- ".github/workflows/vdom-fuzz.yml"
- "Cargo.lock"
- "Cargo.toml"
- "codecov.yml"
- "packages/fuzz/**"
- "packages/oracle/**"
- "packages/core/**"
- "packages/core-types/**"
- "packages/dioxus/**"
- "packages/ssr/**"
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches:
- main
paths:
- ".github/workflows/vdom-fuzz.yml"
- "Cargo.lock"
- "Cargo.toml"
- "codecov.yml"
- "packages/fuzz/**"
- "packages/oracle/**"
- "packages/core/**"
- "packages/core-types/**"
- "packages/dioxus/**"
- "packages/ssr/**"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
CARGO_INCREMENTAL: 0
CARGO_TERM_COLOR: always
FUZZ_DIR: packages/fuzz/fuzz
FUZZ_TARGET: vdom_ops
RUST_BACKTRACE: 1
rust_nightly: nightly-2025-10-05
# `cargo fuzz run` (smoke test) and `cargo fuzz coverage` both
# produce binaries that trip LSan on `generational_box`'s and the
# fuzz harness's intentional `Box::leak` sites. Apply the same
# suppression file to every step that runs the fuzzer.
LSAN_OPTIONS: suppressions=${{ github.workspace }}/packages/fuzz/fuzz/lsan.supp,print_suppressions=1
jobs:
test-and-coverage:
if: github.event.pull_request.draft == false
name: "Fuzz | Test and coverage"
runs-on: warp-ubuntu-latest-x64-4x
timeout-minutes: 45
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v5
- name: Install Rust ${{ env.rust_nightly }}
uses: dtolnay/rust-toolchain@nightly
with:
toolchain: ${{ env.rust_nightly }}
components: llvm-tools-preview
- uses: dtolnay/install@cargo-fuzz
- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
cache-workspace-crates: "true"
cache-provider: "warpbuild"
- name: Test fuzz support crate
run: cargo test -p dioxus-vdom-fuzz --lib --examples
- name: Resolve LSan symbolizer
# Both the smoke test and `cargo fuzz coverage` need this on PATH
# for LSan to demangle symbols (and therefore for the `leak:`
# suppressions to match). Write it to $GITHUB_ENV so every later
# step inherits it.
run: |
target_triple="$(rustc +${{ env.rust_nightly }} -vV | sed -n 's/^host: //p')"
rustup_symbolizer="$(rustc +${{ env.rust_nightly }} --print sysroot)/lib/rustlib/$target_triple/bin/llvm-symbolizer"
if [ -x "$rustup_symbolizer" ]; then
symbolizer="$rustup_symbolizer"
elif command -v llvm-symbolizer >/dev/null; then
symbolizer="$(command -v llvm-symbolizer)"
else
symbolizer="$(ls /usr/bin/llvm-symbolizer-* 2>/dev/null | sort -V | tail -n1)"
fi
if [ -z "$symbolizer" ] || [ ! -x "$symbolizer" ]; then
echo "no usable llvm-symbolizer found" >&2
exit 1
fi
echo "ASAN_SYMBOLIZER_PATH=$symbolizer" | tee -a "$GITHUB_ENV"
- name: Smoke test fuzz target
run: |
mkdir -p "$RUNNER_TEMP/fuzz-corpus" "$RUNNER_TEMP/fuzz-artifacts"
cargo +${{ env.rust_nightly }} fuzz run --fuzz-dir "$FUZZ_DIR" "$FUZZ_TARGET" "$RUNNER_TEMP/fuzz-corpus" -- \
-runs=256 \
-artifact_prefix="$RUNNER_TEMP/fuzz-artifacts/"
- name: Generate fuzz coverage
id: coverage
env:
# Smoke test already runs LSan; the coverage build is purely for
# llvm-cov instrumentation. With leak detection on, cargo-fuzz's
# MERGE-mode child exits non-zero even when every leak is
# suppressed, which fails the step despite the profdata being
# written successfully.
ASAN_OPTIONS: detect_leaks=0
run: |
cargo +${{ env.rust_nightly }} fuzz coverage --fuzz-dir "$FUZZ_DIR" "$FUZZ_TARGET" "$RUNNER_TEMP/fuzz-corpus" -- -runs=0
target_triple="$(rustc +${{ env.rust_nightly }} -vV | sed -n 's/^host: //p')"
llvm_cov="$(rustc +${{ env.rust_nightly }} --print sysroot)/lib/rustlib/$target_triple/bin/llvm-cov"
# cargo-fuzz's coverage mode overrides --target-dir to
# `<cwd>/target/<triple>/coverage` (see project.rs::target_dir),
# so the binary lands in the workspace-root `target/`, not in
# `$FUZZ_DIR/target/`.
coverage_binary="target/$target_triple/coverage/$target_triple/release/$FUZZ_TARGET"
coverage_profile="$FUZZ_DIR/coverage/$FUZZ_TARGET/coverage.profdata"
coverage_lcov="$RUNNER_TEMP/fuzz.lcov"
coverage_report="$RUNNER_TEMP/fuzz-coverage.txt"
coverage_comment="$RUNNER_TEMP/fuzz-coverage.md"
test -x "$coverage_binary"
test -s "$coverage_profile"
"$llvm_cov" report \
--instr-profile="$coverage_profile" \
"$coverage_binary" \
--sources packages/fuzz/src \
| tee "$coverage_report"
"$llvm_cov" export \
--format=lcov \
--instr-profile="$coverage_profile" \
"$coverage_binary" \
--sources packages/fuzz/src \
> "$coverage_lcov"
test -s "$coverage_lcov"
test -s "$coverage_report"
COVERAGE_REPORT="$coverage_report" \
COVERAGE_COMMENT="$coverage_comment" \
python3 - <<'PY'
import os
import sys
from pathlib import Path
report_path = Path(os.environ["COVERAGE_REPORT"])
comment_path = Path(os.environ["COVERAGE_COMMENT"])
output_path = Path(os.environ["GITHUB_OUTPUT"])
total = next(
(line for line in report_path.read_text(encoding="utf-8").splitlines() if line.startswith("TOTAL")),
None,
)
if total is None:
print("llvm-cov report did not include a TOTAL row", file=sys.stderr)
sys.exit(1)
fields = total.split()
if len(fields) < 10:
print(f"Unexpected llvm-cov TOTAL row: {total}", file=sys.stderr)
sys.exit(1)
comment = f"""## Dioxus VDOM fuzz coverage
Coverage generated from `cargo fuzz coverage` for `packages/fuzz/src` after the `{os.environ["FUZZ_TARGET"]}` smoke corpus run.
| Metric | Coverage |
| --- | ---: |
| Regions | {fields[3]} |
| Functions | {fields[6]} |
| Lines | {fields[9]} |
"""
comment_path.write_text(comment, encoding="utf-8")
with output_path.open("a", encoding="utf-8") as output:
output.write("comment<<EOF\n")
output.write(comment)
output.write("\nEOF\n")
PY
- name: Upload fuzz coverage to Codecov
uses: codecov/codecov-action@v5
with:
fail_ci_if_error: true
files: ${{ runner.temp }}/fuzz.lcov
flags: fuzz
name: fuzz
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload fuzz coverage artifact
if: always()
uses: actions/upload-artifact@v6
with:
name: fuzz-coverage
path: |
${{ runner.temp }}/fuzz.lcov
${{ runner.temp }}/fuzz-coverage.txt
${{ runner.temp }}/fuzz-coverage.md
if-no-files-found: ignore
retention-days: 7
- name: Upload fuzz failure artifacts
if: failure()
uses: actions/upload-artifact@v6
with:
name: fuzz-artifacts
path: ${{ runner.temp }}/fuzz-artifacts
if-no-files-found: ignore
retention-days: 7