Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01456qmqCGpCWJkrg6JUZNNM
5.6 KiB
Testing
Where a new test goes, the validation bar and --no-fail-fast are in
AGENTS.md. This page covers the layers, harnesses and baselines.
Layers
- Unit tests, inline beside a lowering, predicate, certificate or fold.
- Crate integration tests in
crates/<crate>/tests/, through the public surface only (crates/r2engine/tests/engine/native.rsis the model). - Golden files in
tests/db/, radare2's r2r format, run bycrates/r2s/tests/golden.rs: a command's whole output on a fixed binary, so a pull request's diff shows what moved (tests/db/README.md). - The claim oracle,
crates/r2engine/tests/engine/oracle.rs, described below. - End-to-end harnesses that run a built
r2sover real binaries.
The claim oracle
The oracle checks every claim pd and pdf make against runs of the
function's own lift, with r2il::eval as the machine: one concrete byte state
per run, one step per operation as the P-code reference defines it, and nothing
imported from an analysis. Constant folds in r2ssa answer through the same
r2il::eval::apply, so the oracle checks the semantics the engine folds with.
- Entry states give every register read zero, one, all ones, each constant the body names and its neighbours, and seeded randoms; P-code booleans get only zero or one. The stack pointer and specification-tracked registers are pinned. A second run per state seeds every word read before written.
- A run stops at a call, an unmapped access, an unexecutable operation or its budget, and rules on nothing after that.
- A claim no run rules on fails, so an unchecked claim never passes; a second
test injects the overclaims each check exists to reject. A new claim kind
does not compile until
verdictsays how a run rules on it.
Harnesses
certify_render.py, diff_r2.py and run_equiv.py read target/debug/r2s
unless given --r2s; the first two build it first unless given --no-build,
and run_coverage.sh builds and uses the release binary.
| Question | Harness |
|---|---|
| Does every named function render, reading nothing never written? | python3 scripts/certify_render.py --bins <radare2>/test/bins/elf --limit 24 --functions 8 |
| Where do discovery, naming and decoding disagree with radare2? | python3 scripts/diff_r2.py --bins <radare2>/test/bins/elf --limit 30 |
| How much of each corpus binary renders, against a baseline? | ./tests/coverage/run_coverage.sh [--accept-baseline] |
| Does the rendered C compute what the binary computes (x86-64 Linux)? | tests/equiv/run_equiv.py --r2s target/debug/r2s --baseline tests/equiv/baseline.json |
| What does DecBench score, on its own protocol? | tests/decbench/run_decbench.sh (see its README) |
| Did any rendering move, base against head? | python3 scripts/census.py run --r2s <r2s> --out <dir> per side, then census.py diff <base> <head> --report <file> (CI: Census + Timing vs Base) |
| Did a large function get slower? | python3 scripts/census.py time --r2s <base> --r2s <head> --bins <radare2>/test/bins/elf (fails above 1.3x in CI) |
How does pdd cost grow with function size (LX's exit)? |
python3 scripts/census.py exponent --r2s <r2s> --max-exponent 1.05 <binary>... (Linux, perf) |
| What holds the heap at its peak, and which sites grow faster than the function? | cargo build --profile profiling -p r2s --features sleigh,dhat-heap, run R2S_HEAP_PROFILE=1 r2s -q -c 'pdd @ <f>' <binary> (writes dhat-heap.json), then python3 scripts/census.py heap [<instructions>=]<dhat-heap.json>... |
| How robust is one large binary? | tests/coverage/sweep_binary.sh <binary> |
scripts/setup_corpus.py builds local corpora (coreutils, CGC, Juliet) under
/tmp/r2sleigh-corpora. Keep binaries and reports out of the repository, and
keep reports sorted so two revisions diff.
A disagreement with radare2 may be radare2's defect; judge it, never match it.
Baselines
tests/coverage/coverage-baseline.json records, per function, whether it
rendered and the typed cause when it did not. A function that rendered and now
refuses fails the gate; one that now renders needs --accept-baseline.
tests/equiv/baseline.json says what each rendering computes; every record
that is not equal carries its cause, and run_equiv.py exits 3 until a
baseline is blessed with --write-baseline <path>.
Re-bless a baseline only after reading the new output and judging it correct, never because it differs. Agreement with a recorded output is evidence that nothing moved, not that the output is right; a score is never semantic proof.
Closed pipes
A test that closes the read end of a child's output and expects the child's next write to fail (exit 141 in
crates/r2s/tests/commands.rs) races every other thread of its test binary that spawns. A spawn in flight holds a
copy of each descriptor the process has open until its exec completes, which takes milliseconds under load, so the
pipe stays open there and the child's write succeeds: the test saw exit 0 in 159 of 600 trials in a reproduction that
closed the pipe of a sh after its prompt while eight threads spawned r2s, and in 0 of 600 when the closing thread
held the spawn lock from the close to the exit. Every spawn in that binary goes through started, which takes
SPAWNING, and the two pipe tests hold it from the close until the child has exited.
Diagnostics
R2DEC_TRACE_REFUSAL=1 prints every refusal_evidence! site with its
predicate, file, line and operands. Use it to find where a refusal happens,
then a debugger to see why. pdil (the lift) and pddo (the obligations)
narrow a defect to the lift or the rendering.