name: rust-test-style
description: GenVM Rust test conventions and style. Use when writing, adding, or modifying Rust tests — inline #[cfg(test)] mod tests, integration tests under a crate's tests/, helpers, assertions, and how ya-test-runner discovers them.
Writing Rust tests in GenVM
Standard library testing only. Do not add rstest, proptest, insta, or
pretty_assertions — they are not used anywhere in the repo. Plain #[test],
#[cfg(test)] mod tests, and std assertions are the whole toolkit. tokio is
available with the macros feature, so use #[tokio::test] when a test must be
async (rare; no async tests exist today).
To run these tests, see the /test skill (--filter-tag rust).
Where tests go
Two locations, both in use:
- Integration tests — one file per concern under the crate's
tests/dir. Preferred for exercising a crate's public API. Eachtests/*.rsfile is its own compilation unit and gets its own runner case.- e.g.
executor/crates/calldata/tests/derive_decode.rs,executor/crates/common/tests/expr.rs,modules/implementation/tests/test_rat.rs
- e.g.
- Inline unit tests —
#[cfg(test)] mod tests { ... }at the bottom of asrc/*.rsfile, for testing private items. e.g.executor/crates/calldata/src/lib.rs,executor/crates/common/src/logger/mod.rs.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nested_value_in_struct() { /* ... */ }
}
Helpers over fixtures
There is no fixture framework. Factor repeated setup into small free functions
at the top of the test file, then keep each #[test] short. This is the
dominant pattern.
// executor/crates/common/tests/expr.rs:5
fn eval(input: &str) -> Value {
Expr::parse(input).unwrap().evaluate().unwrap()
}
fn assert_rational(val: Value, n: i64, d: i64) {
let r = val.into_rational().unwrap();
assert_eq!(r, BigRational::new(BigInt::from(n), BigInt::from(d)));
}
// modules/implementation/tests/test_rat.rs — construct the system under test
fn make_vm() -> Lua {
let vm = Lua::new();
rat::register_rat_global(&vm).unwrap();
vm
}
fn eval<T: mlua::FromLua>(vm: &Lua, code: &str) -> T {
vm.load(code).eval::<T>().unwrap()
}
Define throwaway #[derive(...)] types as fixtures right in the test file:
// executor/crates/calldata/tests/derive_decode_excess_fields.rs:11
#[derive(Debug, PartialEq, Decode)]
struct Named { x: i32, y: String }
Assertions
std only: assert!, assert_eq!, assert_ne!. When asserting a boolean
condition, add a format-string message that prints the actual value so a
failure is debuggable:
let msg = err.to_string();
assert!(msg.contains("unknown field"), "unexpected error: {msg}");
assert!(msg.contains("`z`"), "should mention field name: {msg}");
For value equality use assert_eq! against a fully-constructed expected value
(derive Debug, PartialEq on the type).
Testing errors
Use a helper that returns Result, call .unwrap_err(), and assert on
substrings of the Display message — don't match on error enum internals:
// executor/crates/calldata/tests/derive_decode_excess_fields.rs:5
fn try_decode_from_value<T: codec::Decode>(val: Value) -> Result<T, codec::DecodeError> {
T::decode(codec::ValueDeserializer(val))
}
#[test]
fn tuple_struct_rejects_too_many_elements() {
let err = try_decode_from_value::<Tuple>(/* 3 elems */).unwrap_err();
assert!(err.to_string().contains("expected 2"), "unexpected error: {err}");
}
The happy path is liberal with .unwrap() — a panic is the test failure.
Roundtrip pattern
For codecs, encode → decode → compare in one helper:
// executor/crates/calldata/tests/derive_decode.rs:5
fn roundtrip_via_value<T>(val: &T) -> Value
where T: for<'a> codec::Encode<&'a mut Vec<u8>, Error = std::convert::Infallible> {
let mut buf = Vec::new();
codec::Encode::encode(val, &mut Encoder::new(&mut buf)).unwrap();
genlayer_calldata::decode(&buf).unwrap()
}
Naming & layout
- Test fn names are descriptive
snake_casethat name the scenario, e.g.named_struct_rejects_unknown_field,string_literals_and_escapes. - Do not prefix with
test_. The#[test]attribute already says it is a test; the prefix is noise. Drop it from new tests and from any you touch. - Group related tests with a section-comment banner:
// ── Tuple struct: wrong sequence length ───────────────────────────── - Helpers first, then types, then
#[test]functions.
How ya-test-runner discovers Rust tests
Discovery lives in tests/runner/ya_test_runner_plugins/cargo.py. For each
registered Rust crate root it creates cases via plain cargo test:
| Source | Command | Tags |
|---|---|---|
each tests/*.rs file |
cargo test --test <file> |
rust, unit |
crate has src/lib.rs (inline #[cfg(test)]) |
cargo test --lib |
rust, unit |
each binary (src/main.rs / [[bin]]) |
cargo test --bin <name> |
rust, unit |
each [[example]] |
cargo check --example <name> (compile-only) |
rust, example |
Consequences when adding tests:
- A new file in
tests/is auto-discovered — no registration needed, as long as the crate root itself is already scanned. - New inline
#[cfg(test)]tests ride along on the existing--libcase; nothing to register. - The crate must be a known Rust root. Roots carry a
.ya-test-config.json(e.g.executor/.ya-test-config.json,executor/crates/calldata/.ya-test-config.json). That file controls per-cratecargo_test_flags,keep_env, and askipmap (skipexamples/specific cases) — add a new crate's config there if introducing a new root. - The
rustpreset is(rust | integration) & !bench & !fuzz(tests/presets/rust.txt).
Prefer fuzzing when it fits
If a function's correctness is really about holding over a space of inputs
(parsers, codecs, encode/decode roundtrips, anything taking arbitrary bytes or
structured input), write a fuzz target rather than a handful of hand-picked
#[test] cases. A few cherry-picked examples give false confidence; a fuzzer
explores the space. Don't settle for an under-tested #[test] when the property
is fuzz-shaped — reach for fuzz, or do both (fuzz for coverage, a couple of
#[test]s pinning known edge cases / regressions).
Fuzz targets live under a crate's fuzz/ (afl, behind the arbitrary feature,
tagged fuzz, built with cargo_afl_build_flags; see cargo_fuzz in
tests/runner/ya_test_runner_plugins/cargo.py). They are a distinct case type —
don't mix a fuzz harness into tests/ or mod tests, and they're excluded from
the rust preset (!fuzz).