Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
f0e1ba0
Add VDOM fuzzing harness
ealmloff May 19, 2026
bbb1778
use the test case builder
ealmloff May 19, 2026
69a490c
move over more tests
ealmloff May 19, 2026
d265fbe
fuzzing passes
ealmloff May 20, 2026
7c8d1ab
cache templates
ealmloff May 20, 2026
7e58391
simplify oracle
ealmloff May 20, 2026
45d36a1
normalize dynamic nodes instead of matching many times
ealmloff May 20, 2026
1e530e0
ignore fuzz logs
ealmloff May 20, 2026
13da663
simplify op model
ealmloff May 20, 2026
09fcce5
add ci workflow
ealmloff May 20, 2026
e43d87a
capture fuzz panics
ealmloff May 20, 2026
277163c
100% fuzzing code coverage for core diffing
ealmloff May 20, 2026
c0148dc
trim strategies from fuzzer
ealmloff May 20, 2026
9f7df13
fix component drop order
ealmloff May 20, 2026
785a936
fix component drop order
ealmloff May 20, 2026
c8f97d7
minimize the diff a bit
ealmloff May 21, 2026
7d4e474
restore oracle as a dev dep
ealmloff May 21, 2026
d654677
fuzzing passes
ealmloff May 21, 2026
c8e4aca
remove pending_* states from fuzzer
ealmloff May 21, 2026
6943bde
cover more event surface
ealmloff May 21, 2026
54935c5
ignore recursive event calls for now
ealmloff May 21, 2026
b8c2fff
clarify debug assert
ealmloff May 21, 2026
474c32f
simplify attribute merging
ealmloff May 21, 2026
3ef3d18
simplify node diff
ealmloff May 21, 2026
7260909
remove dynamic_node_is_rendered_in_dom
ealmloff May 21, 2026
84dead8
move oracle
ealmloff May 21, 2026
0030137
wip new attribute diff
ealmloff May 21, 2026
0260035
split out attribute diffing
ealmloff May 21, 2026
38ebaa3
clean up diff
ealmloff May 21, 2026
7461a01
add a note about the new debug assert
ealmloff May 21, 2026
af38b30
clean up crash files
ealmloff May 21, 2026
a5813ad
ignore crash files
ealmloff May 21, 2026
1d8ca2c
documentation for the new attribute diffing behavior
ealmloff May 21, 2026
cc99931
remove OptimizedStrategy
ealmloff May 22, 2026
1f86e9a
simplify suspense
ealmloff May 22, 2026
149d03d
more trims
ealmloff May 22, 2026
76dbf6b
fix raw attribute sorting
ealmloff May 22, 2026
198a9aa
sort with runtime names
ealmloff May 22, 2026
2c2e5ee
remove Sequence and thread locals
ealmloff May 22, 2026
fc9bf05
move listener tracking logic out of oracle
ealmloff May 22, 2026
4cfb2e1
revert scope_should_render
ealmloff May 22, 2026
1c20008
rely on sorted spread attributes
ealmloff May 22, 2026
2de66a7
remove sorted range
ealmloff May 22, 2026
0b2a133
remove dead case
ealmloff May 22, 2026
9e632e5
move panic_message into fuzz
ealmloff May 22, 2026
9d6f5e9
remove VNodeEdit
ealmloff May 22, 2026
19b6ff3
move over the rest of the core tests
ealmloff May 22, 2026
ecb8d97
switch suspense to use the oracle
ealmloff May 22, 2026
cc14d36
add more comments to attributes
ealmloff May 22, 2026
5cfdb2b
fix fuzzer
ealmloff May 22, 2026
154ebdb
symbolic ignores
ealmloff May 22, 2026
ef83a9d
fuzz CI: probe llvm-symbolizer location
ealmloff May 22, 2026
1b52cc4
fuzz CI: gitignore .claude session state
ealmloff May 22, 2026
0c51c26
move options up
ealmloff May 22, 2026
cdab9aa
exclude fuzz from tests
ealmloff May 22, 2026
1ab8ab4
fix clippy
ealmloff May 22, 2026
8a72fdd
more clippy fixes
ealmloff May 22, 2026
a8e9bd9
disable leak detection for coverage
ealmloff May 22, 2026
b85185b
fix coverage ci
ealmloff May 22, 2026
4a42f80
force more collisions in fuzzing
ealmloff May 22, 2026
f15f52b
fix pr comment workflow
ealmloff May 22, 2026
796d613
fix fmt
ealmloff May 22, 2026
e94d71e
fix code cov comment
ealmloff May 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .github/workflows/vdom-fuzz-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: VDOM Fuzz Comment

# Downloads the `fuzz-coverage` artifact produced by `VDOM Fuzz` and posts
# its rendered markdown to the originating PR as a tagged comment that
# upserts on rerun. Runs in base-repo context with `pull-requests: write`
# so it can comment on PRs from forks.
#
# This workflow never checks out the head SHA or runs fork code; its only
# fork input is the markdown body inside the artifact.

on:
workflow_run:
workflows: ["VDOM Fuzz"]
types: [completed]

permissions:
pull-requests: write
actions: read

jobs:
comment:
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Download fuzz-coverage artifact
uses: actions/download-artifact@v6
with:
name: fuzz-coverage
path: artifact
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}

- name: Resolve PR number
id: pr
uses: actions/github-script@v7
with:
script: |
// workflow_run.pull_requests is populated only for same-repo
// PRs; for fork PRs we look the PR up by its head ref.
const run = context.payload.workflow_run;
if (run.pull_requests && run.pull_requests.length > 0) {
core.setOutput('number', run.pull_requests[0].number);
return;
}
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${run.head_repository.owner.login}:${run.head_branch}`,
});
if (prs.length === 0) {
core.setFailed(
`no open PR found for head ${run.head_repository.owner.login}:${run.head_branch}`,
);
return;
}
core.setOutput('number', prs[0].number);

- name: Post fuzz coverage comment
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3
with:
pr-number: ${{ steps.pr.outputs.number }}
filePath: artifact/fuzz-coverage.md
comment-tag: fuzz-coverage
229 changes: 229 additions & 0 deletions .github/workflows/vdom-fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
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
# Directory the coverage report and PR comment are scoped to. The fuzz
# target exercises core's diffing algorithm, so the meaningful number is
# how much of that algorithm got executed.
FUZZ_COVERAGE_SOURCES: packages/core/src/diff
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 "$FUZZ_COVERAGE_SOURCES" \
| tee "$coverage_report"

"$llvm_cov" export \
--format=lcov \
--instr-profile="$coverage_profile" \
"$coverage_binary" \
--sources "$FUZZ_COVERAGE_SOURCES" \
> "$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 of `{os.environ["FUZZ_COVERAGE_SOURCES"]}` from `cargo fuzz coverage` for 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
13 changes: 12 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,20 @@ node_modules/

# ignore the output of tmps
tmp/
.tmp**/

# in debugging we frequently dump wasm to wat with `wasm-tools print`
*.wat

# External macos drives have extra ._ files
._*
._*

# Fuzzing logs
fuzz-*.log

# LibFuzzer failure artifacts
/crash-*
/timeout-*
/oom-*
/leak-*
.claude/
Loading
Loading