docs(bench,test): READMEs for benches/ and tests/

benches/README.md: catalog of the two bench families (cross-runtime
comparisons + the v0.5 run-queue shootout), how to run the shootout
(scripts/bench_rq.sh, SMARM_BENCH_* knobs, the --no-default-features dance,
summary.csv/RQCSV format), honest-numbers caveats (core count is the
experiment; striped losing at low thread counts is expected), and the
conventions for adding a bench.

tests/README.md: catalog grouped by layer (low-level units / feature areas /
regression+stress), the run matrix (debug-first — that's where the invariant
asserts live — three queue variants, release, loom, trace), why loom tests
live in src/ rather than tests/, and the house conventions: one runtime per
test, oversubscription on purpose, regression tests validated against the
reintroduced bug, odds-stacking for stochastic tests, ordering-not-duration
time assertions, and new-invariant = assert + loom model.

Also fixes a stale header in tests/mutex.rs that claimed 'loom::Mutex' —
it tests smarm::Mutex, and the old name is actively confusing now that loom
is real in this repo.
This commit is contained in:
Claude
2026-06-09 21:35:08 +00:00
parent 039703dbeb
commit a7ca6646d7
3 changed files with 211 additions and 1 deletions
+120
View File
@@ -0,0 +1,120 @@
# Tests
Integration tests for the runtime. Each file owns one feature area or one
class of bug. Everything here runs under plain `cargo test`; the loom model
tests are the exception — they live **in the library** (`src/slot_state.rs`,
`src/run_queue.rs`), not in this directory, because loom must compile the
production code with shimmed atomics (see "Loom" below).
## Running
```
cargo test # debug build — RUN THIS ONE: all invariant asserts live
cargo test --release # what users actually execute (LTO, no debug_asserts)
```
Debug builds are not just "slower tests": the runtime self-checks its
invariants only there — every `StateWord` transition asserts its
precondition, `enqueue` asserts the exact `(gen, Queued)` word, `RawMutex`
enforces the never-two-cold-locks leaf rule with a per-thread held-count,
`live_actors` checks for double-finalize underflow. A green release run with
a red debug run means an invariant broke without (yet) corrupting behavior —
treat it as a real failure.
### The queue-variant matrix
The run queue is compile-time selected; the suite must pass under all three
(features are additive, so drop the default first):
```
cargo test # rq-mutex (default)
cargo test --no-default-features --features rq-mpmc
cargo test --no-default-features --features rq-striped
```
### Loom (model checking)
```
RUSTFLAGS="--cfg loom" cargo test --lib --release
```
Exhaustively explores interleavings of the slot state machine
(`src/slot_state.rs`: lost-wakeup, at-most-once-enqueue, the stale-pid ABA
theorem, unpark-vs-claim) and the ring queues (`src/run_queue.rs`:
exactly-once through lap wraparound, push/pop races). Models run the
production transitions through `src/sync_shim.rs` — std atomics normally,
`loom::sync` under `--cfg loom`. `RawMutex` is deliberately not modeled:
futexes can't be, and it's the textbook Drepper mutex3 with stress and
unwind-safety tests of its own.
### Trace feature
`cargo test --features smarm-trace` exists mainly to catch bit-rot in the
`te!()` call sites; run it after touching scheduler paths.
### Before a runtime-core PR
The full matrix, in rough order of bug-finding power per minute:
1. `cargo test` (debug, default variant)
2. debug under `rq-mpmc` and `rq-striped`
3. `cargo test --release`
4. loom
5. `cargo build --features smarm-trace`
## Catalog
**Low-level units (no scheduler)**
| file | covers |
|---|---|
| `context.rs` | `init_actor_stack` + the naked-asm context-switch shims, poked directly |
| `stack.rs` | the mmap'd stack allocator |
| `pid.rs` | pid packing/equality |
**Feature areas (run under a real runtime)**
| file | covers |
|---|---|
| `runtime.rs` | `Config`, `Runtime::run`, re-running a runtime, correctness under genuine parallelism |
| `scheduler.rs` | spawn / join / panic delivery / `yield_now` / `self_pid` |
| `channel.rs` | send/recv (recv parks, so these need the runtime) |
| `selective_recv.rs` | `recv_match` / `try_recv_match` |
| `mutex.rs` | the actor-blocking `Mutex<T>` (lock parks) |
| `timer.rs` | `sleep` ordering — time-sensitive, generous tolerances by design |
| `io.rs` | `block_on_io`: blocking closures on the pool while the actor parks |
| `io_epoll.rs` | `wait_readable` / `wait_writable` + the `read`/`write` sugar |
| `preempt.rs` | explicit preemption via `smarm::check!()` |
| `cancel.rs` | cooperative cancellation (`request_stop`) — the keystone semantics |
| `monitor.rs` | `monitor` delivers exactly one `Down`; `demonitor` |
| `link.rs` | bidirectional links + `trap_exit` |
| `supervisor.rs` | one-for-one supervision |
| `gen_server.rs` | call/cast round-trips, lifecycle callbacks, server-down detection |
**Regression & stress**
| file | covers |
|---|---|
| `stress.rs` | lost wakeups, pid-table pressure, thundering herds, panic isolation under concurrency. Where the phase-2 RefCell-migration bug was caught. |
| `poison_stop.rs` | `request_stop` racing an alloc-under-lock must not poison/abort. See its header for the full story. |
| `many_timers_multi_thread.rs` | multi-thread sleep-timer lost-wakeup regression |
## Conventions
- **Each test owns its runtime.** `init(Config::exact(N))` + `rt.run(...)`;
never share a `Runtime` between tests. Oversubscription (`exact(4)` on one
core) is deliberate — forced interleaving at yield points is how
single-core CI finds races at all.
- **Regression tests must be validated against the bug.** A regression test
that passes with the bug reintroduced is documentation, not a test.
Reintroduce the fix's inverse locally and watch it fail before trusting it
(`poison_stop.rs` went through exactly this: its first version never fired
the sentinel under a lock, and was rewritten until it SIGABRT'd pre-fix).
- **Stochastic tests get the odds stacked.** Use `Config::alloc_interval(1)`
to make every allocation an observation point, many actors, and both
phases of any every-other-allocation cadence (see
`poison_stop::self_stop_during_spawn...`).
- **Time-based assertions use ordering, not durations.** Assert
"didn't return instantly" / "A woke before B", with generous tolerances;
CI machines are slow and noisy.
- New invariants added to the runtime should come with the assert at the
point of reliance (debug_assert on hot paths) *and*, where the invariant is
a protocol, a loom model in the owning module — that combination is what
made phases 25 land without a single post-merge race so far.