Files
Claude a7ca6646d7 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.
2026-06-09 21:35:08 +00:00

121 lines
5.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.