docs-internal/engine/TEST_SNAPSHOTS.md
Generate and load RocksDB snapshots of the full UniversalDB KV store for integration and migration tests.
The test-snapshot-gen crate (engine/packages/test-snapshot-gen/) provides:
test-snapshot-gen) that runs scenarios to generate RocksDB snapshots.test_snapshot) that loads those snapshots into test infrastructure.Snapshots capture the entire UDB state (epoxy, gasoline, pegboard, etc.) for each replica in a multi-node cluster. They are stored as raw RocksDB checkpoint directories checked in as normal fixture files.
# List available scenarios
cargo run -p test-snapshot-gen -- list
# Build a specific scenario
cargo run -p test-snapshot-gen -- build epoxy-v1
Each scenario produces a single snapshot directory:
engine/packages/test-snapshot-gen/snapshots/{scenario}/
metadata.json # commit, branch, timestamp
replica-1/ # RocksDB checkpoint
replica-2/ # RocksDB checkpoint
Regenerating a scenario overwrites the previous snapshot in place.
engine/packages/test-snapshot-gen/src/scenarios/.Scenario trait:
name() - unique scenario name (used as directory name, e.g. "epoxy-v1")replica_count() - number of replicas in the clusterpopulate() - write state through normal APIs (epoxy propose, UDB transactions, etc.)scenarios::all().cargo run -p test-snapshot-gen -- build <name> and commit the result.To generate a snapshot that captures state from a different code version, you need to run the generator on a branch where that code version exists. The scenario code itself must also exist on that branch.
The typical workflow:
epoxy_keys.rs).# Create a worktree from the branch with the target code version
git worktree add /tmp/rivet-main main
# Copy the test-snapshot-gen crate into the worktree
cp -r engine/packages/test-snapshot-gen /tmp/rivet-main/engine/packages/test-snapshot-gen
# Add it to the worktree's Cargo.toml workspace members and dependencies
# Build and run the scenario in the worktree
cd /tmp/rivet-main
cargo run -p test-snapshot-gen -- build epoxy-v1
# Copy the snapshot back to your feature branch
cp -r engine/packages/test-snapshot-gen/snapshots/epoxy-v1 \
/path/to/feature-branch/engine/packages/test-snapshot-gen/snapshots/epoxy-v1
# Clean up
git worktree remove /tmp/rivet-main
If your scenario only writes data through stable APIs that haven't changed between versions (e.g. propose::Input), you can generate the snapshot directly on your feature branch instead.
Add test-snapshot-gen as a dev-dependency:
[dev-dependencies]
test-snapshot-gen.workspace = true
The simplest way to load a snapshot is with SnapshotTestCtx, which boots a full multi-replica cluster from snapshot data:
use test_snapshot::SnapshotTestCtx;
#[tokio::test(flavor = "multi_thread")]
async fn my_migration_test() {
// Load snapshot and start replicas (no coordinator).
let mut test_ctx = SnapshotTestCtx::from_snapshot("epoxy-v1")
.await
.unwrap();
let replica_id = test_ctx.leader_id;
let ctx = test_ctx.get_ctx(replica_id);
// Run your workflow, read data, assert results...
test_ctx.shutdown().await.unwrap();
}
If your test also needs the epoxy coordinator running:
let mut test_ctx = SnapshotTestCtx::from_snapshot_with_coordinator("epoxy-v1")
.await
.unwrap();
For custom setups, use load_snapshot directly:
use test_snapshot::load_snapshot;
let test_id = uuid::Uuid::new_v4();
let replica_paths = load_snapshot("epoxy-v1", test_id).unwrap();
// replica_paths: HashMap<ReplicaId, PathBuf>
// Each path is a temp copy of the snapshot RocksDB, ready for setup_single_datacenter.
TestCluster (same infrastructure as epoxy integration tests).populate() writes state through normal APIs.universaldb::Database::checkpoint().metadata.json file is written with the commit hash, branch name, and timestamp.$TMPDIR/rivet-test-{test_id}-{dc_label}, which is the same path that rivet_test_deps::setup_single_datacenter creates. Since the directory already exists with data, the RocksDB driver opens it and finds the pre-populated state.All files under engine/packages/test-snapshot-gen/snapshots/ are checked in directly. Keep scenarios small enough that generated snapshots stay lightweight.