Testing
Run
Tests run under nextest (the same runner CI uses), plus a separate doc-test pass since nextest does not run doc tests:
cargo nextest run --workspace # unit + integration tests
cargo test --doc --workspace # doc tests
cd ui && bun run lint # ui typecheck
Install the runner once with cargo install cargo-nextest (or see the
nextest install docs). just test runs
both Rust passes for you. Plain cargo test --workspace still works if you
haven’t installed nextest, but CI runs nextest so prefer it locally.
The Ollama Cloud live smoke sends a billed request and is ignored by default:
OLLAMA_API_KEY=... ROLTER_OLLAMA_LIVE_MODEL=gpt-oss:20b \
cargo test -p rolter-gateway --test ollama_cloud live_smoke -- --ignored
Test grouping is configured in .config/nextest.toml:
the Postgres-backed rolter-store/rolter-control suites share one database and
reset the schema per test, so they run in a single-threaded group to avoid
clobbering each other.
Layout
- Unit tests live next to the code in
#[cfg(test)] mod tests. Current coverage: balancer strategies (round-robin cycling, consistent-hash stability, cache-aware affinity, empty targets), the prefix trie, config parsing, model rewrite, auth checks, and the in-memory store. - Keep the pure crates (
rolter-core,rolter-balancer,rolter-auth) fully unit-testable without I/O.
Strategy as the project grows
- Integration tests for the gateway: spin up the Axum app with a mock upstream (
wiremock/httpmock) and assert routing, auth, model rewrite, error mapping and streaming passthrough. - Property tests (
proptest) for the balancer: distribution fairness, affinity invariants. - DB tests for
rolter-storePostgres backend behind a feature, using a disposable container. - Load tests (
oha/k6) against a mock upstream to track added latency and max RPS (see performance.md).
Benchmarks
Hot-path micro-benchmarks run under criterion. They live in crates/<crate>/benches/ with a [[bench]] harness = false entry per file, and cover the per-request cost that shows up as pure gateway overhead:
just bench # cargo bench --workspace
cargo bench -p rolter-balancer # just the balancer benches
cargo bench -p rolter-balancer --bench pick # one bench target
Current coverage (rolter-balancer):
pick—LoadBalancer::pickfor every built-in strategy over a ~24-target pool with a populatedRouteContext.trie— prefix-trieinsert(bounded/unbounded, so LRU eviction is measured) andlongest_prefixon a warm trie.
criterion writes HTML reports to target/criterion/. Benches are not run in CI (timings are noisy on shared runners), but cargo clippy --workspace --all-targets -- -D warnings compiles them on every PR, so they cannot silently bit-rot. Use just bench-check (cargo bench --workspace --no-run) to compile them locally without running.
Coverage
Workspace line coverage is measured with
cargo llvm-cov:
cargo install cargo-llvm-cov
cargo llvm-cov --workspace --all-features --summary-only # quick %
cargo llvm-cov --workspace --all-features --html # browsable report
CI runs coverage in the coverage job of quality.yml and enforces a
ratcheting baseline: the committed baseline lives in
.github/coverage-baseline.txt, and
.github/scripts/coverage-ratchet.sh
fails the step if the current percentage drops more than
COVERAGE_TOLERANCE points (default 0.5) below it. The job also uploads the
lcov.info report as a CI artifact.
Policy (ROL-246):
- New code must not push coverage below
baseline − tolerance. If a PR legitimately lowers coverage, edit.github/coverage-baseline.txtin the same PR and explain why. - When coverage climbs well above the baseline, raise the baseline to lock in the gain (the ratchet only goes up).
- The job is informational (
continue-on-error: true) until the baseline is trusted; promote it to blocking by removing that flag on thecoveragejob.
CI
.github/workflows/ci.yml delegates to the shared quality.yml gate, which runs cargo fmt --check, cargo clippy -D warnings, cargo nextest run --workspace --all-features plus a cargo test --doc pass, the feature matrix, cargo doc (warnings as errors), cargo-deny, gitleaks, the UI lint/build, and a Conventional Commit PR-title check on every push/PR.
Full-stack compose smoke
The compose-smoke job boots the production-shaped Docker Compose topology
(Postgres, Redis, ClickHouse, gateway, control) and exercises it end-to-end. Run
it locally with the same script CI uses:
bash docker/smoke/smoke.sh
It layers docker/docker-compose.ci.yml
over the base compose file: the overlay mounts
docker/smoke/rolter.smoke.toml (a
keyless open gateway config) so the built-in fake-llm model answers without any
provider secret. The script waits for both /healthz endpoints, checks
/v1/models and fake-llm chat (non-streaming + SSE) on the gateway and the
postgres-backed /internal/snapshot on the control plane, then always dumps
compose logs and runs down -v. It is informational (continue-on-error)
until the image-build cost and flake profile are trusted (ROL-245).