Fuzzing and deep property testing¶
How cuere explores its renderers and wallet-URI builders beyond the everyday test run — and which coverage-guided fuzzing tools it does (and does not) adopt, and why.
What's already in place¶
cuere's correctness rests on three layers that run on every change:
- Property-based tests (Hypothesis) in
tests/test_render_properties.pyandtests/test_wallet.py. They round-trip arbitrary bit-grids and real segno encoder output through the renderers, and exercise the wallet-URI builders /optimize_uriover generated inputs. The defaultdevprofile draws 50 examples per test; selection is byHYPOTHESIS_PROFILE(tests/conftest.py). - 100% branch coverage, enforced in CI (
fail_under = 100). Every branch is executed by the suite by construction. - Mutation testing (mutmut, dev-only) — proof that the assertions, not just the coverage, would catch a behavioral change.
Coverage-guided fuzzing is a fourth, deeper layer: instead of drawing inputs blindly, it uses execution feedback to steer toward inputs that reach new states. The question this page answers is which OSS tool, if any, is worth adopting for that — given the layers above already make cuere's covered code hard to surprise.
HypoFuzz is a personal local aid, not a project tool
HypoFuzz runs the existing Hypothesis tests as a
coverage-guided fuzzer with zero new harness, which is ideal — but it is
proprietary (LicenseRef-HypoFuzz: free for non-commercial / open-source
use, no redistribution, commercial/CI use needs a paid licence). Declaring it
in pyproject.toml / uv.lock would publish it into the dependency graph and
trip the dependency-review licence gate. It is therefore deliberately not
a declared dependency — it lives only in an uncommitted, git-ignored
fuzz-local.sh that installs it transiently (uv run --with hypofuzz …). See
#37. This page evaluates an
OSS-licensed path to the same goal.
Options evaluated¶
| Option | Licence | Reuses @given tests? |
Dependency weight | Fit |
|---|---|---|---|---|
| Wide Hypothesis profile | MPL-2.0 (already a dependency) | ✅ directly | none | High — pure-OSS, no new harness |
| Atheris (Google) | Apache-2.0 (allow-listed) | ❌ needs a TestOneInput(bytes) harness |
heavy — native, clang/libFuzzer build | Moderate — genuine coverage feedback, at a real cost |
| OSS-Fuzz (service) | n/a | ❌ Atheris harness | n/a | No — admission requires significant userbase / criticality |
Wide Hypothesis profile — a high-max_examples, generous-deadline profile
that reuses the existing property tests and runs non-blocking (scheduled and/or
local), keeping the per-PR suite fast. Cheapest possible adoption: Hypothesis is
already a dependency, and there is no second input-generation surface to maintain.
Atheris — a libFuzzer-based, genuinely coverage-guided fuzzer. It does not
consume @given tests; it needs a separate TestOneInput(data: bytes) harness
(typically via FuzzedDataProvider). Its licence (Apache-2.0) passes the
dependency-review allow-list, so — unlike HypoFuzz — it is not blocked on
licensing. The cost is elsewhere: it is a heavy native dependency (clang/libFuzzer
at build) and a second input-generation surface to maintain alongside the
Hypothesis strategies.
OSS-Fuzz — Google's continuous-fuzzing service. It builds on Atheris harnesses and is aimed at projects with a significant user base or critical role; cuere would not currently qualify, so it is out of scope here.
Decision¶
cuere pursues both OSS paths as tracked follow-ups, and keeps HypoFuzz as a personal local aid only:
- Pilot Atheris first — #96. Add a minimal harness for the highest-value targets (the renderers and the wallet-URI builders), keep it dev/local-only or a non-blocking scheduled job, and record a keep-or-drop verdict here once measured. Because cuere already enforces 100% branch coverage, the upside is bounded to finding bad values within already-covered branches, so the pilot must demonstrate value beyond the existing Hypothesis suite to earn a permanent place — it did (see Pilot outcome below).
- Add a wide Hypothesis profile —
#97. The low-cost,
zero-new-dependency companion: a
nightly-style profile reusing the existing property tests, run off the blocking PR path. - HypoFuzz stays local — proprietary, never a declared dependency (#37).
- OSS-Fuzz is not pursued at cuere's current scale.
As part of recording this decision, the dead ci Hypothesis profile (registered
at 200 examples but never selected) was removed from tests/conftest.py; the real
wide profile lands with #97.
Pilot outcome¶
The Atheris pilot (#96) is
kept. A minimal harness lives in
tests/fuzz/ —
fuzz_render.py (renderers) and fuzz_wallet.py (wallet-URI builders /
optimize_uri), plus a pytest smoke test. Atheris is declared in a dev-only
fuzz dependency group, kept out of the default uv sync, the runtime, and the
blocking CI path (it is a heavy native, clang/libFuzzer, build).
Why keep it. The pilot did exactly what the keep criterion required — it
surfaced a defect class the Hypothesis suite misses. Within a minute the wallet
target found #99 (since
fixed): a huge-exponent amount/value (e.g. "1E999999999") passed the
finiteness and sign checks, then raised an unhandled MemoryError — a denial of
service — instead of the documented WalletURIError, because the
magnitude-dependent format() / int() ran before the value was bounded.
That is precisely the
"bad value within an already-covered branch" that 100% branch coverage cannot
rule out. The renderer target, by contrast, ran clean (~133k execs, no crash),
consistent with the renderers being fully exercised by the property tests.
How it runs (dev/local-only; see tests/fuzz/README.md):
uv sync --group fuzz
uv run --group fuzz pytest tests/fuzz -m atheris --no-cov # short smoke run
uv run --group fuzz python tests/fuzz/atheris/fuzz_wallet.py -max_total_time=300
Operating model. The harness runs on demand (local) for now; promoting it to a non-blocking scheduled job is a reasonable future step, not a pilot requirement. HypoFuzz stays local-only (#37); the wide Hypothesis profile (#97) remains the cheap, zero-new-dependency companion.