diff --git a/Cargo.lock b/Cargo.lock index 2af905e..e00e0a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1326,7 +1326,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.114", ] [[package]] @@ -1744,6 +1744,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -5467,7 +5473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.114", @@ -6617,6 +6623,7 @@ name = "testing-framework-runner-local" version = "0.1.0" dependencies = [ "async-trait", + "fs_extra", "tempfile", "testing-framework-core", "thiserror 2.0.18", @@ -7564,7 +7571,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/logos/runtime/ext/src/lib.rs b/logos/runtime/ext/src/lib.rs index 8701c6c..0ae97fc 100644 --- a/logos/runtime/ext/src/lib.rs +++ b/logos/runtime/ext/src/lib.rs @@ -89,6 +89,25 @@ impl LocalDeployerEnv for LbcExtEnv { ) } + fn build_node_config_from_template( + topology: &Self::Deployment, + index: usize, + peer_ports_by_name: &HashMap, + options: &StartNodeOptions, + peer_ports: &[u16], + template_config: Option<&::NodeConfig>, + ) -> Result::NodeConfig>, DynError> { + let mapped_options = map_start_options(options)?; + ::build_node_config_from_template( + topology, + index, + peer_ports_by_name, + &mapped_options, + peer_ports, + template_config, + ) + } + fn build_initial_node_configs( topology: &Self::Deployment, ) -> Result::NodeConfig>>, ProcessSpawnError> { @@ -103,6 +122,14 @@ impl LocalDeployerEnv for LbcExtEnv { ::initial_persist_dir(topology, node_name, index) } + fn initial_snapshot_dir( + topology: &Self::Deployment, + node_name: &str, + index: usize, + ) -> Option { + ::initial_snapshot_dir(topology, node_name, index) + } + fn build_launch_spec( config: &::NodeConfig, dir: &Path, @@ -135,6 +162,7 @@ fn map_start_options( mapped.peers = options.peers.clone(); mapped.config_override = options.config_override.clone(); mapped.persist_dir = options.persist_dir.clone(); + mapped.snapshot_dir = options.snapshot_dir.clone(); Ok(mapped) } diff --git a/testing-framework/core/src/scenario/capabilities.rs b/testing-framework/core/src/scenario/capabilities.rs index e0220ea..fc056e4 100644 --- a/testing-framework/core/src/scenario/capabilities.rs +++ b/testing-framework/core/src/scenario/capabilities.rs @@ -42,6 +42,8 @@ pub struct StartNodeOptions { Option Result + Send + Sync>>, /// Optional persistent working directory for this node process. pub persist_dir: Option, + /// Optional directory whose contents should seed the node working dir. + pub snapshot_dir: Option, _phantom: PhantomData, } @@ -52,6 +54,7 @@ impl fmt::Debug for StartNodeOptions { .field("config_override", &self.config_override.is_some()) .field("config_patch", &self.config_patch.is_some()) .field("persist_dir", &self.persist_dir) + .field("snapshot_dir", &self.snapshot_dir) .finish() } } @@ -63,6 +66,7 @@ impl Default for StartNodeOptions { config_override: None, config_patch: None, persist_dir: None, + snapshot_dir: None, _phantom: PhantomData, } } @@ -95,6 +99,12 @@ impl StartNodeOptions { self.persist_dir = Some(persist_dir); self } + + #[must_use] + pub fn with_snapshot_dir(mut self, snapshot_dir: PathBuf) -> Self { + self.snapshot_dir = Some(snapshot_dir); + self + } } /// Indicates whether a capability requires node control. diff --git a/testing-framework/deployers/local/Cargo.toml b/testing-framework/deployers/local/Cargo.toml index ce7c833..cbd0b97 100644 --- a/testing-framework/deployers/local/Cargo.toml +++ b/testing-framework/deployers/local/Cargo.toml @@ -14,6 +14,7 @@ workspace = true [dependencies] async-trait = "0.1" +fs_extra = "1.3" tempfile = { workspace = true } testing-framework-core = { path = "../../core" } thiserror = { workspace = true } diff --git a/testing-framework/deployers/local/src/env.rs b/testing-framework/deployers/local/src/env.rs index 2af7d06..602c661 100644 --- a/testing-framework/deployers/local/src/env.rs +++ b/testing-framework/deployers/local/src/env.rs @@ -35,6 +35,17 @@ where peer_ports: &[u16], ) -> Result::NodeConfig>, DynError>; + fn build_node_config_from_template( + topology: &Self::Deployment, + index: usize, + peer_ports_by_name: &HashMap, + options: &StartNodeOptions, + peer_ports: &[u16], + _template_config: Option<&::NodeConfig>, + ) -> Result::NodeConfig>, DynError> { + Self::build_node_config(topology, index, peer_ports_by_name, options, peer_ports) + } + fn build_initial_node_configs( topology: &Self::Deployment, ) -> Result::NodeConfig>>, ProcessSpawnError>; @@ -47,6 +58,14 @@ where None } + fn initial_snapshot_dir( + _topology: &Self::Deployment, + _node_name: &str, + _index: usize, + ) -> Option { + None + } + fn build_launch_spec( config: &::NodeConfig, dir: &Path, @@ -90,6 +109,7 @@ pub async fn spawn_node_from_config( config: ::NodeConfig, keep_tempdir: bool, persist_dir: Option<&std::path::Path>, + snapshot_dir: Option<&std::path::Path>, ) -> Result, ProcessSpawnError> { ProcessNode::spawn( &label, @@ -98,6 +118,7 @@ pub async fn spawn_node_from_config( E::node_endpoints, keep_tempdir, persist_dir, + snapshot_dir, E::node_client, ) .await diff --git a/testing-framework/deployers/local/src/node_control/mod.rs b/testing-framework/deployers/local/src/node_control/mod.rs index dc31d5f..324878a 100644 --- a/testing-framework/deployers/local/src/node_control/mod.rs +++ b/testing-framework/deployers/local/src/node_control/mod.rs @@ -19,11 +19,12 @@ mod state; use state::LocalNodeManagerState; #[derive(Clone)] -struct NodeStartSnapshot { +struct NodeStartSnapshot { peer_ports: Vec, peer_ports_by_name: HashMap, node_name: String, index: usize, + template_config: Option, } #[derive(Debug, Error)] @@ -83,12 +84,14 @@ impl NodeManager { for (index, config_entry) in configs.into_iter().enumerate() { let persist_dir = E::initial_persist_dir(descriptors, &config_entry.name, index); + let snapshot_dir = E::initial_snapshot_dir(descriptors, &config_entry.name, index); spawned.push( spawn_node_from_config::( config_entry.name, config_entry.config, keep_tempdir, persist_dir.as_deref(), + snapshot_dir.as_deref(), ) .await?, ); @@ -113,6 +116,7 @@ impl NodeManager { clients_by_name: HashMap::new(), indices_by_name: HashMap::new(), nodes: Vec::new(), + template_config: None, }; Self { @@ -146,6 +150,9 @@ impl NodeManager { pub fn stop_all(&self) { let mut state = self.lock_state(); + for node in &mut state.nodes { + node.start_kill(); + } state.nodes.clear(); state.peer_ports.clone_from(&self.seed.peer_ports); @@ -155,6 +162,7 @@ impl NodeManager { state.clients_by_name.clear(); state.indices_by_name.clear(); state.node_count = self.seed.node_count; + state.template_config = None; self.node_clients.clear(); } @@ -228,12 +236,13 @@ impl NodeManager { ) -> Result, NodeManagerError> { let snapshot = self.start_snapshot(name)?; - let mut built = E::build_node_config( + let mut built = E::build_node_config_from_template( &self.descriptors, snapshot.index, &snapshot.peer_ports_by_name, &options, &snapshot.peer_ports, + snapshot.template_config.as_ref(), ) .map_err(|source| NodeManagerError::Config { source })?; @@ -250,6 +259,7 @@ impl NodeManager { built.network_port, built.config, options.persist_dir.as_deref(), + options.snapshot_dir.as_deref(), ) .await?; @@ -290,12 +300,14 @@ impl NodeManager { network_port: u16, config: ::NodeConfig, persist_dir: Option<&std::path::Path>, + snapshot_dir: Option<&std::path::Path>, ) -> Result { let node = spawn_node_from_config::( node_name.to_string(), config, self.keep_tempdir, persist_dir, + snapshot_dir, ) .await .map_err(|source| NodeManagerError::Spawn { @@ -306,6 +318,9 @@ impl NodeManager { self.node_clients.add_node(client.clone()); let mut state = self.lock_state(); + if state.template_config.is_none() && snapshot_dir.is_some() { + state.template_config = Some(node.config().clone()); + } state.register_node(node_name, network_port, client.clone(), node); @@ -322,7 +337,10 @@ impl NodeManager { reinsert_node_at(&mut state, index, node); } - fn start_snapshot(&self, requested_name: &str) -> Result { + fn start_snapshot( + &self, + requested_name: &str, + ) -> Result, NodeManagerError> { let state = self.lock_state(); let index = state.node_count; let node_name = validate_new_node_name::(state.node_count, &state, requested_name)?; @@ -332,6 +350,7 @@ impl NodeManager { peer_ports_by_name: state.peer_ports_by_name.clone(), node_name, index, + template_config: state.template_config.clone(), }) } @@ -349,6 +368,7 @@ fn clear_registered_nodes(state: &mut LocalNodeManagerState state.clients_by_name.clear(); state.indices_by_name.clear(); state.node_count = 0; + state.template_config = None; } fn validate_new_node_name( diff --git a/testing-framework/deployers/local/src/node_control/state.rs b/testing-framework/deployers/local/src/node_control/state.rs index d3dd1c3..59e3f1e 100644 --- a/testing-framework/deployers/local/src/node_control/state.rs +++ b/testing-framework/deployers/local/src/node_control/state.rs @@ -9,6 +9,7 @@ pub(crate) struct LocalNodeManagerState { pub(crate) clients_by_name: HashMap, pub(crate) indices_by_name: HashMap, pub(crate) nodes: Vec>, + pub(crate) template_config: Option, } impl LocalNodeManagerState { diff --git a/testing-framework/deployers/local/src/process.rs b/testing-framework/deployers/local/src/process.rs index 12bb91c..9e3df42 100644 --- a/testing-framework/deployers/local/src/process.rs +++ b/testing-framework/deployers/local/src/process.rs @@ -10,6 +10,7 @@ use std::{ time::Duration, }; +use fs_extra::dir::{CopyOptions, copy as copy_dir}; use tempfile::TempDir; use testing_framework_core::{env::Application, process::RuntimeNode, scenario::DynError}; use tokio::{ @@ -112,6 +113,11 @@ pub enum ProcessSpawnError { #[source] source: io::Error, }, + #[error("failed to copy snapshot directory: {source}")] + Snapshot { + #[source] + source: io::Error, + }, #[error("process wait failed: {source}")] Wait { #[source] @@ -192,9 +198,14 @@ impl NodeEndpoints, keep_tempdir: bool, persist_dir: Option<&Path>, + snapshot_dir: Option<&Path>, client_from_endpoints: impl FnOnce(&NodeEndpoints) -> Client, ) -> Result { let tempdir = create_tempdir(persist_dir)?; + if let Some(snapshot_dir) = snapshot_dir { + copy_snapshot_dir(snapshot_dir, tempdir.path()) + .map_err(|source| ProcessSpawnError::Snapshot { source })?; + } let launch = build_launch_spec(&config, tempdir.path(), label) .map_err(|source| ProcessSpawnError::Config { source })?; @@ -328,6 +339,16 @@ fn write_launch_file(base: &Path, file: &LaunchFile) -> io::Result<()> { fs::write(path, &file.contents) } +fn copy_snapshot_dir(from: &Path, to: &Path) -> io::Result<()> { + let mut options = CopyOptions::new(); + options.copy_inside = true; + options.overwrite = true; + + copy_dir(from, to, &options) + .map(|_| ()) + .map_err(io::Error::other) +} + fn default_api_socket() -> SocketAddr { SocketAddr::from((Ipv4Addr::LOCALHOST, 0)) }