Tests¶
Test structure and the patterns new tests follow. Individual tests are not catalogued here. The source tree is the catalogue, and a per-test list would go stale.
Layers¶
| Layer | Location | Coverage |
|---|---|---|
| Backend unit | #[cfg(test)] mod tests beside the code |
Pure logic, no I/O |
| Backend integration | backend/gradient-*/tests/*.rs |
The real router and handlers over a mocked database |
| Shared harness | backend/gradient-test-support/ |
Fakes, fixtures and the test server reused by every suite |
| CLI | cli/tests/*.rs |
The gradient binary against a stub HTTP server |
| Frontend | frontend/src/**/*.spec.ts |
Components and services under vitest |
| Report inspector | nix/tools/report-inspector/tests/ |
Inspector commands over a report fixture built in the test |
| NixOS VM | nix/tests/gradient/<name>/, nix/tests/harness/ |
A booted machine running the packaged server, or a module against a scripted API |
| SQL plan check | backend/src/sql_gate/, started by the e2e VM test |
Every registered statement's plan at production scale |
| Mock daemon | backend/gradient-daemon/, nix/tests/store-spec/, nix/tests/gradient/scheduler/, nix/tests/gradient/cluster/ |
Scheduler and workers against a scripted Nix store |
| Eval benchmark | nix/tests/bench/evalbench/, on request |
Evaluation speed per stage, with a capture bundle per round (Eval Benchmark) |
- A crate's
tests/directory is for public entry points (an HTTP route, a CLI call). All other tests go into a#[cfg(test)]module next to the code. - The inspector fixture is built in the test, never committed as a
.db. A checked-in binary would drift from its schema.
Running¶
| Command | Checks |
|---|---|
nix flake check |
Every check below |
nix build .#checks.x86_64-linux.unittest |
Backend tests (nextest, then doc tests) |
nix build .#checks.x86_64-linux.cli-unittest |
CLI tests |
nix build .#checks.x86_64-linux.gradient-<name> |
One VM test |
nix build .#gradient-evalbench |
The eval benchmark, outside nix flake check |
pnpm -C frontend exec ng test --watch=false |
Frontend tests |
Warning
A local cargo test or cargo nextest over the whole backend workspace would need more memory than a typical workstation has. The Nix checks are the supported way.
- The cargo suites are checks, not the packages' check phase.
nix build .#gradientwill build the binary only. - Tests build under
[profile.test], unoptimised and without the full DWARF of the released binary.[profile.dev.package."*"]will keep dependencies optimised. - Any folder under
nix/tests/gradient/will become the checkgradient-<folder>without wiring. - Every VM test will store NARs on local disk. S3 storage has no VM test.
Shared Harness¶
gradient-test-support can spare every suite its own scaffolding. A missing helper is added to the crate rather than re-derived locally. use gradient_test_support::prelude::* will bring in the common items.
| Module | Contents |
|---|---|
fakes/ |
In-memory doubles: FakeNixStoreProvider, FakeDerivationResolver, FakeWorkerStore, FakeDrvReader, RecordingJobReporter, RecordingCiReporter, InMemoryEmailSender |
fakes/store_fixture.rs |
StoreFixture: a real 951-derivation hello closure from backend/test-store/, with helpers to mark subtrees built or unbuild a fraction |
fixtures.rs |
Canonical entity models and stable IDs (project(), user(), eval_at(), ...) |
db.rs |
db_with(rows), the MockDatabase setup |
state.rs |
test_state() and variants: a wired ServerState |
web.rs |
make_test_server(), make_token(), live_session() for authenticated requests |
cli.rs |
test_cli() |
log_storage.rs |
NoopLogStorage, RecordingLogStorage |
cache_fixture.rs |
Populated cache and NAR state |
The prelude will also re-export the protocol doubles.
MockProtoServer(gradient-wire) can script an authority with handshake, offers, scores, claims and NAR passthrough.ProtoPeer(gradient-worker-client) can script a worker on the real client connection.
Patterns¶
Scope¶
- Test what we wrote. A test asserting that serde serialised a field, or that SeaORM returned the handed row, would only cover a dependency. A test whose deletion would change nobody's confidence is not added.
- Fakes, not a mock framework. Code touching nix, git, the filesystem or the network must sit behind a trait. Fakes implement the trait and record calls. Recording fakes let a test assert the sequence of effects.
- Fakes for one algorithm stay local. Some traits only lift one algorithm out of its I/O (
UpstreamIoin the worker's substitute executor). Such a trait will keep its fake in that module's tests. - Actors with side effects sit behind small traits. The
gradient-effectsactor can takePendingDeliveryStoreandHandoffas inputs. The actor's one promise is at most the worker count in flight, with one pass per burst of wakes. In-memory fakes are asserting this promise with no database and no clock. - Closure and scheduling logic must use
StoreFixture, a genuine derivation graph instead of a hand-built three-node tree.
MockDatabase¶
- Results replay in FIFO order. Each
append_query_resultscall will feed the nextSELECTstatement. The script is the handler's query sequence (auth, loads, the endpoint's work). The module doc comment must state that sequence. INSERT ... RETURNING: Such a statement must have both anappend_query_resultsfor the row and anappend_exec_resultswithrows_affected: 1. SeaORM would otherwise treat the insert as a no-op.- Assert on one statement, never on a formatted transaction. A transaction is one log entry bracketed by a synthetic
BEGIN/COMMITpair.gradient_db::pool::statements(log)will flatten the log to one string per statement without transaction control. - Quoted identifiers or bound values: These need
raw_statements(log)and a match ons.sql/s.values. Thestatementshelper will returnDebugstrings with escaped quotes. - Spawned work races the result buffer. Assertions target the response, not the count of executed queries.
VM Tests¶
- Modules under test get a scripted API. Some modules only consume the HTTP API (
gradient-client/deploy.nix). Such a module is tested against a stdlib-only stub driven through the/controlendpoint. - The stub's
/controlendpoint can swap the scripted state and push the matching WebSocket event. The VM needs no Postgres and no builder. - Assert on the database's own accounting. The e2e test will check plan shapes (
EXPLAIN: nested loop, no merge join). The test will bill the round throughpg_stat_statementsas well. Thresholds stay loose (pathology detectors on a slow VM), and the top statements are printed. - Two database sessions prove a lock. The setup will use two FIFO-fed
psqlsessions from one script. A third connection will pollpg_stat_activityforwait_event_type = 'Lock'rows. The same interleaving is played twice, with and without the lock. The test will assert the contrast.- Every wait is bounded, and the failure will carry a
pg_stat_activitydump. - Both backends are torn down on every exit path. Only synthetic rows are locked.
- Rows offered by no fixture go into a phase-owned schema:
CREATE TABLE <schema>.<t> (LIKE public.<t> INCLUDING DEFAULTS INCLUDING INDEXES)plusSET search_path = <schema>, public. - Statements come from
gradient-sql-gate --print <NAME>, never pasted. A pasted copy would still pass after the code changed.
- Every wait is bounded, and the failure will carry a
SQL Plan Check¶
Every hand-written statement must be declared through gradient_db::sql! with its parameter kinds and tier. backend/clippy.toml will forbid any other way of building a Statement value. The e2e test's last phase will amplify the database to production scale before starting gradient-sql-gate. The check will draw real parameters and explain each statement in a rolled-back transaction.
| Tier | Scope | Extra |
|---|---|---|
Hot |
Default | |
Bulk |
Cost following a working set: dashboards, metric scrapes, batches over id arrays, the build assigner's ranking | |
Walk |
Recursive closure walks | Asserting the OFFSET 0 fence in the recursive term |
Sweep |
Timer-driven work allowed to scan |
- Failures: A sequential scan of a large relation throwing most of its read away. A buffer or amplification budget overrun. A per-row rescan. A disk spill.
- Exceptions: Aggregates, statements returning nothing and batches each have their own rule.
- No wall-clock budgets: The runner is shared and slow.
- Unmeasured statements (empty relations) are reported, not passed. More than 40 will fail the phase.
- Tables filled only by users (stars) are filled through the real API before the check. Values differing per round (commit hash prefixes) are drawn parameter kinds.
- Draws are ordered, never heap order. Every kind except the evaluation will take the oldest rows by id.
- The evaluation is a copy of the evaluation naming the most shared builds, the worst case its statements must fit.
- The copy is newer than the statistics. The check will write the copy after the last ANALYZE, like a new evaluation in production. The planner will estimate the copy from the evaluations outside the most common values. A plain VACUUM after the copy can mark its pages all-visible and keep the statistics untouched.
CLI and Lints¶
- CLI tests drive the real binary. The
assert_cmdcrate will rungradientwithHOMEandXDG_CONFIG_HOMEin aTempDirwith a seededconfig.tomlfile. Thewiremockcrate will stand in for the server. unwraponly with a reason. Both workspaces denyclippy::unwrap_usedcalls. Test scaffolding can opt out per file with the attribute below.
#![expect(
clippy::unwrap_used,
reason = "test scaffolding: a fixture helper that cannot build its value should fail the test loudly"
)]
- Bare
#[allow(unused)],#[allow(dead_code)]and#[allow(unused_imports)]fail CI everywhere, tests included.
Mock Daemon¶
gradient-scheduler and gradient-cluster are running the real server and workers. gradient-daemon serve --backend mock will replace each worker's nix-daemon on the stock socket. Both suites share the server node (scheduler/server.nix) and the script helpers (scheduler/helpers.py). The mock feature of backend/gradient-daemon will build the binary into the daemon output of the gradient package.
| Aspect | Behavior |
|---|---|
| Store spec | A plain attrset: derivations.<id> with deps, outputs.<o>.references ("<node>.<output>"), build.outcome (success, fail, hang), present.workers, present.cache, sameAs (a twin: another .drv with the same name, output paths and FOD content). Defaults and invariants in nix/tests/store-spec/default.nix. Presets chain n, diamond, fanOut n, wide depth width |
| Paths | derivations.nix will feed both the published flake and the daemon config. The store-spec check can assert that drv and output paths agree |
| Downloads | download = true will turn a node into a real <nix/fetchurl.nix> call with an SRI hash. The server node will host its content under /downloads/, and a worker can fetch it without the mock daemon |
| Presence | present.workers is seeded at boot. present.cache will become a signed file cache used as upstream cache |
| Timing | Seeded lognormal delays (median 40 ms). GRADIENT_DAEMON_SEED can replay a round |
| Violations | A build with a missing input, a rebuild of a valid output, an unknown derivation, an unmodelled op. Every phase must end with violations == [] |
| Control | gradient-daemon ctl with the query commands journal, builds, latency, violations and running and the steering commands seed, forget, outcome and release |
| Latency | Each phase will append to latency.jsonl the spreads from can-start to assignment and of builds, the critical path and the overhead factor |
Every reference is inside the runtime closure of the requested inputs, as in Nix.
Production replay: gradient-report <report.db> store-spec -o spec.nix can turn a report into a store spec with edges, references, sizes and scaled durations. The --allow-failed flag will replay failures. The --anonymize flag will rename packages to n0, n1, ...
Topologies¶
The scheduler and e2e suites are mk.nix { self, pkgs, topology }. Their default.nix file will pass in nix/tests/harness/topologies/direct.nix as the topology. The flake will export both as lib.tests.{scheduler,e2e} { system, topology } for other repositories.
- A topology is
{ pkgs, lib, workers, token, ... }: { nodes, upstreamPeers, upstreamUrls?, workerNodes, provides, pythonPrelude }. - The optional
upstreamUrlsattribute can map each upstream peer dialed from the server to its URL. - The
servernode (e2e alsoclient) and each worker's role belong to the suite. - The way workers reach the server and the IDs registered by the server belong to the topology.
nix/tests/harness/contract.nix(lib.tests.contract) is asserted at evaluation. Thetest-topologiescheck incheck.nixwill pin the direct topology.- Scripts use the prelude, never a hardcoded unit or ID:
WORKER_NODES,wait_workers_ready(),fleet_units(),requires(what, *tags). - Some assertions need the server to see each worker directly. These go under
requires(..., "distinct-upstream-workers"). - The
requirescondition is placed on the assertion, not the phase, when later phases need the state. - Both suites run in the proxy repository over its
proxiedandserver-dialstopologies asscheduler-proxied,e2e-proxied,scheduler-server-dialsande2e-server-dials.
Conventions¶
- One file per contract, named after the contract, like
cache_roles.rs,auth_middleware.rsorbody_size_limit.rsfiles. - A module doc comment must state the file's coverage and the harness setup, such as the query script replayed by the mocked database.
- Test names read as the asserted behaviour (
a_refused_session_backs_off). - Regression tests carry a one-line comment naming the defect and the issue number.