Crafting a parser, part 8: testing, snapshots, and fuzzing
A parser is a function from bytes to trees, which makes it one of the most testable things you'll ever write — and one of the easiest to crash with input you didn't imagine. This finale is about proving the parser works and that it never panics.
This wraps Crafting a parser in Rust.
Snapshot tests
Don't hand-write expected ASTs; that is how tests rot. Pretty-print the tree and snapshot it. The first run records the output; later runs diff against it, and you review changes deliberately.
12345
Round-trip and property tests
Two invariants catch a huge class of bugs:
Fuzzing for panics
The contract from part one was "never panic on bad input." Prove it: throw random bytes at the parser and assert it always returns, diagnostics and all.
1fuzz_target!2345;
Deeply nested parens that overflow the stack, multi-byte UTF-8 split across a span, a lone bracket — fuzzers find these in seconds. Cap recursion depth and treat every panic the fuzzer surfaces as a real bug.
Seed the fuzzer's corpus with your snapshot-test inputs. It explores far faster when it starts from real, structurally-valid programs.
The series, one line each
Takeaways
- Snapshot the tree; never hand-maintain expected ASTs.
- Round-trip and idempotence properties catch dropped tokens and printer drift.
- Fuzz to enforce never-panic — and cap recursion depth.
That's a parser, end to end. Build the batch version first; everything else is an upgrade you add when the use case demands it.