-
Notifications
You must be signed in to change notification settings - Fork 3
kithara test macros
Documentation reviewed from source revision 19ca073f2. This records the documented contract at that revision; it is not a new runtime validation. API and usage · All crates.
#[kithara::measure(...)] expands to hotpath::measure only when the caller
enables perf.
A bare #[kithara::test] is a sync test emitted for native + wasm; flags combine (e.g.
#[kithara::test(native, tokio, timeout(Duration::from_secs(5)))]). Every path supports #[case] / #[case::name]
parameterization and #[kithara::fixture] injection.
-
tokio— async test on a manually built native runtime. -
wasm/native— single-platform emission; mutually exclusive. -
browser— browser wasm path; awaitskithara_platform::tokio::ensure_thread_pool()before the body so Web Workers exist. -
timeout(Duration::...)— wall-clock safety net that must fire on REAL time even under flash. Async wraps inplatform::time::timeout; native sync runs the body on a helper thread withrecv_timeout; wasm sync runs it unguarded. -
hang_timeout_secs(N)— the liveness budget#[kithara::hang_watchdog]arms with inside this test, restored on drop. It shortens the REAL wait a watchdog park costs on the flash-off lane (under flash the park is virtual and the ambient budget costs no wall time). One process-global atomic, notKITHARA_HANG_TIMEOUT_SECS: a watched function is entered from whichever thread drives it, so the budget must be visible process-wide, and writing the environment to publish it is undefined behaviour while any other thread reads it. -
tracing("directives")—EnvFilterdirectives, defaultwarn;RUST_LOGstill wins. -
soft_fail("pattern", ...)— catches panics whose message contains any pattern (case-insensitive;"timeout"also matchestimed out) and prints[SOFT FAIL]; other panics resume unwinding. Requiresfuturesat the call site for async tests. -
serial— native tests use#[serial_test::serial]; wasm tests acquire one async test-binary lock before their individual timeout begins, so queued tests do not spend their timeout budget waiting for a prior serial body. -
multi_thread—new_multi_thread().worker_threads(2)instead ofnew_current_thread(); requirestokio. -
selenium— impliesnative + tokio + serial + multi_thread. It injects no#[ignore]: the suite is picked up only by the wasm-target driver (just test run --lane=selenium-firefox). -
loom— synchronous native model test returning(). Incompatible withtokio, async fns, non-unit returns,soft_fail,wasm,browser,selenium, andmulti_thread. -
flash(true|false)— opt the body into or out of flash time rewriting; defaulttrue.
Under flash(true) the macro lexically retargets the body's direct time-primitive calls onto the unconditional
virtual_* variants. The rewriter matches the last two path segments (time::sleep), so a single-segment sleep(...) in
the body is a compile error rather than a silent real-clock call: import the module and call time::sleep(...), or use
flash(false).
rewrite.rs retargets path calls in a test body — Instant::now() becomes
flash::virtual_now(), which reads the engine clock unconditionally. It does not
touch method calls, so started.elapsed() stays Instant::elapsed, which
re-reads now() through the ordinary arm and gets REAL time: a test body never
sets the active flag, which only production #[kithara::flash] regions push.
Subtracting the two therefore mixes a virtual start from a real end. The error is systematic, not jitter — the real arm is measured from the engine's real anchor, so it grows with process age and a wall-clock assertion fails more often the longer the process has lived.
In a test body, sample both endpoints with Instant::now() and subtract them:
Instant::now().saturating_duration_since(started). Never .elapsed(), and never
any other method that internally re-reads now(). The rewrite reaches nested
closures and spawned blocks too, so a timing probe inside tokio_spawn(async move { … }) carries the same rule.
Every emitted body is made flash-eligible by exactly ONE ambient holder. async-native emissions (manual tokio runtime,
with or without timeout) wrap the body in kithara_platform::flash::with_ambient, which re-asserts the ambient around
every poll; they must NOT also hold a body scope, because a body-held ambient_scope lives in the future's state inside
the cancellable timeout and tears down non-LIFO on Elapsed — a stale ambient resurrect, caught by the platform's
restore_mode guard. native sync and wasm emissions instead open the body with a single body-held ambient_scope
(shared::make_ambient_stmt), the sole ambient writer there. Off the flash feature both virtual_* and
ambient_scope are real-aliases / no-ops, so the emitted body stays behaviour-identical.
loom renames the test loom_model_<name> and wraps the body in the hidden kithara_platform::__private::model hook;
lane selection and Loom primitive semantics are owned by kithara-platform contracts.
Do not annotate a full flaky async integration test: Loom cannot model sockets, Tokio scheduling, random input, decoder
FFI, or wall time. Extract the smallest deterministic synchronization contract that explains the flake and exercise it
with kithara::platform::sync, thread, and atomics. A spin/poll loop whose progress needs another modeled thread must
call thread::yield_now. Assertion and Loom panics propagate without catch_unwind; no per-permutation result is
buffered.
Debugging follows Loom's checkpoint contract: just test loom-checkpoint FILE FILTER saves progress while reproducing,
loom-isolate resumes with LOOM_CHECKPOINT_INTERVAL=1 so the file pins the failing permutation, and loom-debug
replays it with LOOM_LOG=trace, LOOM_LOCATION=1, and uncaptured output. The checkpoint JSON is an opaque execution
path, not a report to read by hand; read a replay backward from the first panic or deadlock — Iteration N names the
schedule, thread{id=N} the executing modeled thread, branch switch=true an actual scheduler handoff, the primitive
trace (mutex, atomic, notify, channel) plus its location= field the operation to inspect, and thread_done normal
completion. A branch-limit failure usually means the model is too broad or a fairness-dependent loop is missing
yield_now — shrink the model before raising limits, and set LOOM_MAX_PREEMPTIONS (usually 2 or 3) explicitly for a
deliberately bounded large model.
The probe body is gated cfg(any(test, feature = "probe")) and its emit runs inside a kithara_test_utils::rtsan::permit
guard, so probes stay active but RTSan-transparent under --cfg rtsan.
-
#[kithara::probe](no parens) — marker probe: zero wire args, only the cheap auto-fields (caller_file,caller_line,seq,thread_id,thread_seq,install_id). Use for very-frequent production functions whose parameters are notIntoProbeArg(e.g.Future::poll_next(&self, cx: &mut Context)). -
#[kithara::probe(field1, field2, …)]— parameter idents recorded as wire args; each must match a real parameter name. -
#[kithara::probe(name = expr, …)]— records a computed value under wire-namename.expris evaluated at function entry (so it can readselfand parameters, but not later locals) and must implementIntoProbeArg. A computed name may not collide with a plain ident in the same attribute;callerandprobe_returnare reserved. - Plain and computed entries share one ceiling: 6 wire args total, the USDT provider arity limit. Over it, fold fields
into a
#[derive(kithara::Probe)]struct or split the function. -
#[kithara::probe(caller, …)]— additionally resolvecaller_fnthroughbacktrace::trace. Opt-in because resolution costs roughly a millisecond per firing; do NOT use onpoll_next-style hot probes. -
#[kithara::probe(probe_return)]— records the return value throughProbe::record_probe, emits no entry event, and is the only form that drops#[track_caller].
Use kithara::probe_event!(name, field, wire_name = expression) when the observed
values exist only inside a real product operation. Do not extract a one-call or
one-assignment function just to attach #[kithara::probe].