Skip to content

Commit bd23071

Browse files
kariyclaude
andcommitted
feat(utils): cache migrated TestNode databases as snapshots
Add Db::open_no_sync() for fast test DB loading without durability. Add TestNode::new_from_db() and convenience methods to load pre-migrated database snapshots instead of running the slow clone+build+migrate flow per test. Add generate_migration_db binary to produce the .tar.gz snapshots, and Makefile targets to extract them. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 725dfcf commit bd23071

File tree

6 files changed

+215
-4
lines changed

6 files changed

+215
-4
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Makefile

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ SNOS_DB_DIR := $(DB_FIXTURES_DIR)/snos
1717
COMPATIBILITY_DB_TAR ?= $(DB_FIXTURES_DIR)/1_6_0.tar.gz
1818
COMPATIBILITY_DB_DIR ?= $(DB_FIXTURES_DIR)/1_6_0
1919

20+
SPAWN_AND_MOVE_DB_TAR ?= $(DB_FIXTURES_DIR)/spawn_and_move.tar.gz
21+
SPAWN_AND_MOVE_DB_DIR := $(DB_FIXTURES_DIR)/spawn_and_move
22+
23+
SIMPLE_DB_TAR ?= $(DB_FIXTURES_DIR)/simple.tar.gz
24+
SIMPLE_DB_DIR := $(DB_FIXTURES_DIR)/simple
25+
2026
CONTRACTS_CRATE := crates/contracts
2127
CONTRACTS_DIR := $(CONTRACTS_CRATE)/contracts
2228
CONTRACTS_BUILD_DIR := $(CONTRACTS_CRATE)/build
@@ -62,7 +68,7 @@ snos-artifacts: $(SNOS_OUTPUT)
6268
db-compat-artifacts: $(COMPATIBILITY_DB_DIR)
6369
@echo "Database compatibility test artifacts prepared successfully."
6470

65-
test-artifacts: $(SNOS_DB_DIR) $(SNOS_OUTPUT) $(COMPATIBILITY_DB_DIR) contracts
71+
test-artifacts: $(SNOS_DB_DIR) $(SNOS_OUTPUT) $(COMPATIBILITY_DB_DIR) $(SPAWN_AND_MOVE_DB_DIR) $(SIMPLE_DB_DIR) contracts
6672
@echo "All test artifacts prepared successfully."
6773

6874
build-explorer:
@@ -110,6 +116,18 @@ $(COMPATIBILITY_DB_DIR): $(COMPATIBILITY_DB_TAR)
110116
mv katana_db $(notdir $(COMPATIBILITY_DB_DIR)) || { echo "Failed to extract backward compatibility test database\!"; exit 1; }
111117
@echo "Backward compatibility database extracted successfully."
112118

119+
$(SPAWN_AND_MOVE_DB_DIR): $(SPAWN_AND_MOVE_DB_TAR)
120+
@echo "Extracting spawn-and-move test database..."
121+
@cd $(DB_FIXTURES_DIR) && \
122+
tar -xzf $(notdir $(SPAWN_AND_MOVE_DB_TAR)) || { echo "Failed to extract spawn-and-move test database\!"; exit 1; }
123+
@echo "Spawn-and-move test database extracted successfully."
124+
125+
$(SIMPLE_DB_DIR): $(SIMPLE_DB_TAR)
126+
@echo "Extracting simple test database..."
127+
@cd $(DB_FIXTURES_DIR) && \
128+
tar -xzf $(notdir $(SIMPLE_DB_TAR)) || { echo "Failed to extract simple test database\!"; exit 1; }
129+
@echo "Simple test database extracted successfully."
130+
113131
check-llvm:
114132
ifndef MLIR_SYS_190_PREFIX
115133
$(error Could not find a suitable LLVM 19 toolchain (mlir), please set MLIR_SYS_190_PREFIX env pointing to the LLVM 19 dir)
@@ -180,5 +198,5 @@ snos-deps-macos: install-pyenv
180198

181199
clean:
182200
echo "Cleaning up generated files..."
183-
-rm -rf $(SNOS_DB_DIR) $(COMPATIBILITY_DB_DIR) $(SNOS_OUTPUT) $(EXPLORER_UI_DIST) $(CONTRACTS_BUILD_DIR)
201+
-rm -rf $(SNOS_DB_DIR) $(COMPATIBILITY_DB_DIR) $(SPAWN_AND_MOVE_DB_DIR) $(SIMPLE_DB_DIR) $(SNOS_OUTPUT) $(EXPLORER_UI_DIST) $(CONTRACTS_BUILD_DIR)
184202
echo "Clean complete."

crates/storage/db/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,31 @@ impl Db {
111111
Ok(Self { env, version: CURRENT_DB_VERSION })
112112
}
113113

114+
/// Opens an existing database at the given `path` with [`SyncMode::UtterlyNoSync`] for
115+
/// write performance, similar to [`Db::in_memory`] but on an existing path.
116+
///
117+
/// This is intended for test scenarios where a pre-populated database snapshot needs to be
118+
/// loaded quickly without durability guarantees.
119+
pub fn open_no_sync<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
120+
let version = get_db_version(&path)?;
121+
if version != CURRENT_DB_VERSION && !is_block_compatible_version(&version) {
122+
return Err(anyhow!(DatabaseVersionError::MismatchVersion {
123+
expected: CURRENT_DB_VERSION,
124+
found: version,
125+
}));
126+
}
127+
128+
let env = mdbx::DbEnvBuilder::new()
129+
.max_size(GIGABYTE * 10)
130+
.growth_step((GIGABYTE / 2) as isize)
131+
.sync(SyncMode::UtterlyNoSync)
132+
.build(path)?;
133+
134+
env.create_default_tables()?;
135+
136+
Ok(Self { env, version })
137+
}
138+
114139
// Open the database at the given `path` in read-write mode.
115140
pub fn open<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
116141
Self::open_inner(path, false)

crates/utils/Cargo.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,26 @@ async-trait.workspace = true
1919
futures.workspace = true
2020
rand.workspace = true
2121
thiserror.workspace = true
22-
tokio = { workspace = true, features = [ "macros", "signal", "time" ], default-features = false }
22+
tokio = { workspace = true, features = [ "macros", "rt-multi-thread", "signal", "time" ], default-features = false }
2323

2424
# node-only dependencies
2525
katana-chain-spec = { workspace = true, optional = true }
2626
katana-core = { workspace = true, optional = true }
27+
katana-db = { workspace = true, optional = true }
2728
katana-executor = { workspace = true, optional = true }
2829
katana-node = { workspace = true, optional = true }
2930
katana-provider = { workspace = true, optional = true }
3031
katana-rpc-server = { workspace = true, optional = true }
32+
clap = { workspace = true, optional = true }
3133
starknet = { workspace = true, optional = true }
3234
tempfile = { workspace = true, optional = true }
3335

3436
[features]
3537
node = [
38+
"clap",
3639
"katana-chain-spec",
3740
"katana-core",
41+
"katana-db",
3842
"katana-executor",
3943
"katana-node",
4044
"katana-provider",
@@ -43,3 +47,8 @@ node = [
4347
"tempfile",
4448
]
4549
explorer = [ "node", "katana-node/explorer" ]
50+
51+
[[bin]]
52+
name = "generate_migration_db"
53+
path = "src/bin/generate_migration_db.rs"
54+
required-features = [ "node" ]
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/// Binary to generate pre-migrated database snapshots for testing.
2+
///
3+
/// These snapshots are loaded by `TestNode::new_with_spawn_and_move_db()` and
4+
/// `TestNode::new_with_simple_db()` to avoid the slow migration process (git clone + scarb build +
5+
/// sozo migrate) in each test run.
6+
///
7+
/// Usage:
8+
/// cargo run --bin generate_migration_db --features node -- --example spawn-and-move --output tests/fixtures/db/spawn_and_move.tar.gz
9+
/// cargo run --bin generate_migration_db --features node -- --example simple --output tests/fixtures/db/simple.tar.gz
10+
use std::path::{Path, PathBuf};
11+
use std::process::Command;
12+
13+
use clap::Parser;
14+
use katana_utils::node::{test_config, TestNode};
15+
16+
#[derive(Parser)]
17+
#[command(about = "Generate pre-migrated database snapshots for testing")]
18+
struct Args {
19+
/// The dojo example to migrate (e.g. "spawn-and-move" or "simple").
20+
#[arg(long)]
21+
example: String,
22+
23+
/// Output path for the .tar.gz archive.
24+
#[arg(long)]
25+
output: PathBuf,
26+
}
27+
28+
fn create_tar_gz(db_dir: &Path, output: &Path) -> std::io::Result<()> {
29+
// Derive the directory name from the output file stem (e.g., "spawn_and_move" from
30+
// "spawn_and_move.tar.gz")
31+
let stem = output
32+
.file_stem()
33+
.and_then(|s| s.to_str())
34+
.and_then(|s| s.strip_suffix(".tar"))
35+
.unwrap_or_else(|| {
36+
output.file_stem().and_then(|s| s.to_str()).expect("invalid output file name")
37+
});
38+
39+
let parent = output.parent().expect("output path must have a parent directory");
40+
std::fs::create_dir_all(parent)?;
41+
42+
// Create the archive directory structure expected by the Makefile extraction targets.
43+
// The tar should contain `<stem>/mdbx.dat`, `<stem>/db.version`, etc.
44+
let staging_dir = tempfile::tempdir()?;
45+
let inner_dir = staging_dir.path().join(stem);
46+
std::fs::create_dir_all(&inner_dir)?;
47+
48+
// Copy db files into the staging directory
49+
for entry in std::fs::read_dir(db_dir)? {
50+
let entry = entry?;
51+
if entry.file_type()?.is_file() {
52+
std::fs::copy(entry.path(), inner_dir.join(entry.file_name()))?;
53+
}
54+
}
55+
56+
let status = Command::new("tar")
57+
.args(["-czf"])
58+
.arg(output)
59+
.arg("-C")
60+
.arg(staging_dir.path())
61+
.arg(stem)
62+
.status()?;
63+
64+
if !status.success() {
65+
return Err(std::io::Error::other("tar command failed"));
66+
}
67+
68+
Ok(())
69+
}
70+
71+
#[tokio::main]
72+
async fn main() {
73+
let args = Args::parse();
74+
75+
println!("Starting node with test_config()...");
76+
let node = TestNode::new_with_config(test_config()).await;
77+
78+
println!("Migrating example '{}'...", args.example);
79+
match args.example.as_str() {
80+
"spawn-and-move" => {
81+
node.migrate_spawn_and_move().await.expect("failed to migrate spawn-and-move");
82+
}
83+
"simple" => {
84+
node.migrate_simple().await.expect("failed to migrate simple");
85+
}
86+
other => {
87+
eprintln!("Unknown example: {other}. Supported: spawn-and-move, simple");
88+
std::process::exit(1);
89+
}
90+
}
91+
92+
// Get the database path from the running node
93+
let db_path = node.handle().node().db().path().to_path_buf();
94+
95+
println!("Creating archive at {}...", args.output.display());
96+
let output_abs = std::env::current_dir().unwrap().join(&args.output);
97+
create_tar_gz(&db_path, &output_abs).expect("failed to create tar.gz archive");
98+
99+
println!("Done! Snapshot saved to {}", args.output.display());
100+
}

crates/utils/src/node.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::net::SocketAddr;
2-
use std::path::Path;
2+
use std::path::{Path, PathBuf};
33
use std::process::Command;
44
use std::sync::Arc;
55

@@ -74,6 +74,52 @@ impl TestNode {
7474
.expect("failed to launch node"),
7575
}
7676
}
77+
78+
/// Creates a [`TestNode`] from a pre-existing database directory.
79+
///
80+
/// Copies the database to a temp directory so each test gets its own mutable copy.
81+
/// The database is opened with [`SyncMode::UtterlyNoSync`] for test performance.
82+
pub async fn new_from_db(db_path: &Path) -> Self {
83+
Self::new_from_db_with_config(db_path, test_config()).await
84+
}
85+
86+
/// Creates a [`TestNode`] from a pre-existing database directory with a custom config.
87+
///
88+
/// Copies the database to a temp directory so each test gets its own mutable copy.
89+
/// The database is opened with [`SyncMode::UtterlyNoSync`] for test performance.
90+
pub async fn new_from_db_with_config(db_path: &Path, config: Config) -> Self {
91+
let temp_dir = tempfile::Builder::new()
92+
.disable_cleanup(true)
93+
.tempdir()
94+
.expect("failed to create temp dir");
95+
96+
copy_db_dir(db_path, temp_dir.path()).expect("failed to copy database");
97+
98+
let db = katana_db::Db::open_no_sync(temp_dir.path()).expect("failed to open database");
99+
let provider = DbProviderFactory::new(db.clone());
100+
101+
Self {
102+
node: Node::build_with_provider(db, provider, config)
103+
.expect("failed to build node")
104+
.launch()
105+
.await
106+
.expect("failed to launch node"),
107+
}
108+
}
109+
110+
/// Creates a [`TestNode`] with a pre-migrated `spawn-and-move` database snapshot.
111+
pub async fn new_with_spawn_and_move_db() -> Self {
112+
let db_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
113+
.join("../../tests/fixtures/db/spawn_and_move");
114+
Self::new_from_db(&db_path).await
115+
}
116+
117+
/// Creates a [`TestNode`] with a pre-migrated `simple` database snapshot.
118+
pub async fn new_with_simple_db() -> Self {
119+
let db_path =
120+
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/db/simple");
121+
Self::new_from_db(&db_path).await
122+
}
77123
}
78124

79125
impl ForkTestNode {
@@ -277,6 +323,17 @@ fn run_sozo_migrate(
277323
Ok(())
278324
}
279325

326+
/// Copies all files from `src` to `dst` (flat copy, no subdirectories).
327+
fn copy_db_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
328+
for entry in std::fs::read_dir(src)? {
329+
let entry = entry?;
330+
if entry.file_type()?.is_file() {
331+
std::fs::copy(entry.path(), dst.join(entry.file_name()))?;
332+
}
333+
}
334+
Ok(())
335+
}
336+
280337
pub fn test_config() -> Config {
281338
let sequencing = SequencingConfig::default();
282339
let dev = DevConfig { fee: false, account_validation: true, fixed_gas_prices: None };

0 commit comments

Comments
 (0)