Testing
mushroomdb is a young codebase. This page explains what mechanisms are in place to make “it’s new” a manageable risk rather than a reason to avoid it.
Deterministic simulation testing (DST)
The sim-harness crate replaces real disk IO with SimFs, an in-memory filesystem that
injects crashes at precise byte offsets and Fs-call boundaries. Every test in this section
runs under SimFs; none of them touch the filesystem.
How SimFs works
SimFs has two independent crash modes:
Byte mode (SimFs::with_crash_after(n)) — fires inside append once the cumulative
bytes-written counter crosses n. The torn append writes a prefix and sets a crash latch.
Subsequent append and sync calls return Err; read and write_atomic are not
affected (this models a power loss mid-WAL-write, not a full storage failure).
Op mode (SimFs::with_crash_after_ops(n)) — fires on the n-th Fs call (append, sync,
read, write_atomic combined). A failed write_atomic leaves the old file content in place
(rename-never-happened semantics). This closes the snapshot-write and WAL-truncation crash
windows that byte mode does not reach.
After a crash, surviving_state() returns a clean SimFs with the on-disk bytes the
crash would have left behind, but with the crash latch reset. The recovered GraphDb is
opened from that state.
SimFs unit tests (in crates/sim-harness/src/sim_fs.rs):
crash_tears_the_inflight_appendbyte_crash_does_not_block_read_or_write_atomicop_crash_write_atomic_preserves_old_contentop_crash_blocks_readop_crashed_stays_crashedtorn_append_does_not_count_as_successful_op
Byte-offset crash sweeps
These tests run the workload at every crash point from byte 0 through the last byte
written and verify that recovery is always consistent. “Consistent” means: open_with
never panics or errors, node data and props are either all present or all absent per
commit, no derived edge references a tombstoned node, and rebuild_rule is a no-op.
recovery_is_consistent_at_every_crash_offset
Runs a 20-node chain workload with a mid-stream snapshot. Sweeps all byte offsets.
Bug class caught: torn WAL commits landing partial node-prop records.
recovery_byte_sweep_rules
Runs the full rules workload (KM, OV, DUMMY delete, numeric NW/NZ, geo GEO, exact VEC,
approximate VAPPROX with HNSW; plus batches, ingest with auto-FK, rebuild_rule, and
delete_node of a derived-edge-owning node). Sweeps all byte offsets.
Bug class caught: rule-engine state diverging from node state after mid-stream crash.
cypher_write_dst_byte_sweep
Runs Cypher write statements (CREATE, SET, multi-node CREATE) through the WAL at every
crash point. Verifies the none-or-all invariant for multi-op Batch frames.
Bug class caught: Cypher writes not sharing WAL durability guarantees with API writes.
recovery_delete_heavy_byte_sweep
12 nodes, interleaved deletes and prop updates. Verifies no derived edge references a
tombstoned node at any crash offset.
Bug class caught: retraction gaps when a crash lands between delete and rule-fire.
write_batch_large_frame_dst_byte_sweep
12-op write_batch (insert_node ×5, insert_edge ×2, set_prop ×3 including
rule-triggering ones, remove_prop, delete_node). At every byte offset, verifies
none-or-all atomicity: batch nodes are either all present or all absent.
Bug class caught: partial batch application after WAL frame torn mid-write.
write_batch_composition_sweep
write_batch → snapshot → delete_node (triggers top-k backfill) → write_batch.
Verifies consistency across the snapshot boundary and after top-k backfill fires.
Bug class caught: snapshot-boundary interaction with top-k rule state.
recovery_byte_sweep_views
Materialized view definitions and values across byte-offset crashes.
Bug class caught: view state diverging from node data after partial WAL replay.
recovery_byte_sweep_fulltext and recovery_byte_sweep_fulltext_with_snapshot
Fulltext index across byte-offset crashes with and without a mid-stream snapshot.
Bug class caught: inverted index losing postings after a torn WAL write.
All byte-sweep tests also run an op-mode sweep (recovery_op_sweep_rules) that injects
crashes at every Fs-call boundary, covering snapshot write_atomic and WAL-truncation
write_atomic — the two paths byte mode cannot reach.
Run the byte and op sweep tests:
cargo test -p sim-harness --test crash_recovery -- --nocapture 2>&1 | grep -E "crash points|test result"
Or to run just one sweep with full output:
cargo test -p sim-harness --test crash_recovery recovery_byte_sweep_rules -- --nocapture
Oracle equivalence (scratch-recompute)
After any sequence of mutations, the incremental engine’s derived edge set must equal a from-scratch brute-force evaluation. This is checked continuously by a proptest suite.
Rules oracle: engine_matches_oracle
crates/sim-harness/tests/oracle_equivalence.rs — proptest, 256 cases, sequences up
to 80 ops drawn from: InsertNode, InsertEdge, SetProp, SetF, SetTags, CreateRule
(9 templates: KeyMatch, FieldEqual, Overlap, All, Overlap-shared-etype, NumericWithin ×2,
GeoRadius, VectorSimilar), DeleteRule, DeleteNode, DeleteEdge, RemoveProp, CreateView,
DeleteView, EnableFulltext, DisableFulltext, FulltextSearch, and Batch (2–4 leaf ops).
After every op, the test asserts:
db.stats().nodes_live == oracle.node_count()- All prop values match across all 256 key slots
- Full edge set (user ∪ derived) matches
oracle.all_edges() - Edge weights match
oracle.derived_weights()to within 1e-9 rebuild_ruleis a no-op (re-sweep must equal pre-sweep)- Fulltext search results match
oracle.scratch_search()for all indexed fields and queries
Bug class caught: incremental rule maintenance getting out of sync with the desired derived set after create/delete operations, prop updates, or composed predicates.
Run the proptest oracle suite:
cargo test -p sim-harness --test oracle_equivalence engine_matches_oracle
To run with more cases (slower, but covers more of the state space):
PROPTEST_CASES=1024 cargo test -p sim-harness --test oracle_equivalence engine_matches_oracle
Top-k oracle: topk_dst_sweep
crates/sim-harness/tests/top_k_oracle.rs — proptest, verifies that at every quiescent
point the engine’s per-source out-neighbor set under a top-k rule equals a scratch
recompute of the k best candidates by score (and by key-order tiebreak for unscored rules).
Three rules run concurrently: FieldEqual k=1, NumericWithin k=3, Any(FieldEqual,
NumericWithin) k=2.
Bug class caught: top-k budget enforcement getting the wrong candidates after insert-then-evict, backfill, or score change.
Fixed top-k tests:
topk_dst_insert_evict_and_backfill_sequence— sequential insert/delete sequence that exercises eviction and backfilltopk_dst_numeric_k3_score_order— verifies score ordering under NumericWithintopk_evicted_pair_has_no_explain_entry— evicted edges must not appear inexplain()
Run top-k tests:
cargo test -p sim-harness --test top_k_oracle
View oracle
Inside engine_matches_oracle (proptest): after every op, for every live view, the stored
incremental value is compared to db.scratch_view_value(). Degree and Count use exact
equality; Sum and Avg use a 1e-6 epsilon (disclosed f64 accumulation drift).
Bug class caught: view counter diverging from actual topology after edge retraction or node delete.
Fulltext oracle
Inside engine_matches_oracle (proptest): after every op, db.search(field, query) is
compared to oracle.scratch_search(field, query) for all indexed fields and query strings.
Scratch search tokenizes and evaluates the boolean query independently.
Bug class caught: postings list diverging from actual node content after enable/disable or node mutations.
Fixed fulltext test:
overlap_rule_equality_on_nonempty_derived_sets(query_equivalence.rs) — Cypher MATCH result set matches the traversal API on a non-empty derived edge set
Cypher ↔ traversal equivalence
crates/sim-harness/tests/query_equivalence.rs — proptest. For random small graphs with
an Overlap rule:
MATCH (a {k: $key})-[r:T]->(b) RETURN brow set equalsneighborsOut- 1-hop undirected Cypher row set equals
grouped_by_edge_typebucket
Run:
cargo test -p sim-harness --test query_equivalence
Torn-write tests (WAL unit layer)
crates/sim-harness/src/sim_fs.rs — the torn-write unit tests above verify SimFs
itself. The WAL layer is tested through the crash sweeps: every crash in the byte-mode
sweep represents a potential torn WAL frame. The WAL uses CRC checksums per frame;
recovery_is_consistent_at_every_crash_offset verifies that a torn frame is detected
and discarded rather than partially applied.
Bug class caught: WAL replaying a record with a valid CRC prefix but a missing or corrupt payload.
Replay identity tests
approximate_wal_replay_identity
Builds 20 nodes with 4 vector clusters, creates an approximate (HNSW) rule, captures
the derived edge set, drops and reopens the database (WAL replay), and asserts the edge
set is identical. Same rule + same data → same graph → same edges.
Bug class caught: HNSW state not persisting across a reopen, or non-deterministic candidate selection on replay.
recovery_byte_sweep_rules (also a replay identity test)
At every crash point, open_with(surviving_state) re-materializes derived edges from
scratch and the oracle asserts the result matches brute-force evaluation.
Run replay identity tests:
cargo test -p sim-harness --test oracle_equivalence approximate_wal_replay_identity
Recall floors for approximate mode
VectorSimilar with approximate: true uses HNSW candidate selection. The recall
floor constants are binding (declared in crates/sim-harness/src/lib.rs; do not lower
without explicit sign-off):
APPROX_RECALL_FLOOR_QUIESCED = 0.90 # fully quiesced: all nodes indexed, HNSW built
APPROX_RECALL_FLOOR_RECOVERY = 0.85 # any crash-recovery or early HNSW build state
Tests:
approximate_recall_above_floor_quiesced— 20 2-D unit vectors in 4 clusters; asserts per-query recall ≥ 0.90 on a fully built index.approximate_recall_above_floor_after_rebuild— same dataset, asserts recall ≥ 0.85 after an explicitrebuild_rule.topk_approx_recall_floor— approximate rule inside the top-k oracle sweep; asserts recall ≥ 0.90 at every quiescent point.- In
recovery_byte_sweep_rules, every crash-recovery state withvec_approxlive asserts recall ≥APPROX_RECALL_FLOOR_RECOVERY.
Run recall tests:
cargo test -p sim-harness --test oracle_equivalence approximate_recall
cargo test -p sim-harness --test top_k_oracle topk_approx_recall_floor
Running everything
Full Rust gate (required before every commit touching crates/):
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo build --workspace --examples
cargo test --workspace
cargo bench --no-run
All five commands must exit 0. The bench step is compile-only.
To run only the sim-harness suite:
cargo test -p sim-harness
To run with output (useful for the sweep progress lines):
cargo test -p sim-harness -- --nocapture 2>&1 | grep -E "crash points|recall|test result"
What is not yet covered
- Multi-statement transactions — not implemented; no test exists or is needed until the feature ships.
- Concurrent writer correctness — the engine is single-writer by design (
RwLockfacade). Race conditions between writers are excluded by the architecture, not tested. - Network-layer correctness — the HTTP and WebSocket layers have golden-shape tests
in
crates/server/tests/; they do not yet have fault-injection or load tests. - Cypher coverage — the supported subset and its known gaps are documented in
docs/site/query.md.