diff --git a/sim-cli/src/config.rs b/sim-cli/src/config.rs new file mode 100644 index 00000000..3a7928c7 --- /dev/null +++ b/sim-cli/src/config.rs @@ -0,0 +1,209 @@ +use std::{path::PathBuf, str::FromStr}; + +use anyhow::Context; +use log::LevelFilter; +use serde::Deserialize; + +use crate::Cli; + +#[derive(Debug)] +pub struct SimulationConfig { + // Path to a directory where simulation results are save + pub data_dir: PathBuf, + // Path to simulation file + pub sim_file: PathBuf, + // Duration for which the simulation should run + pub total_time: Option, + // Number of activity results to batch together before printing to csv file [min: 1] + pub print_batch_size: u32, + /// Level of verbosity of the messages displayed by the simulator. + /// Possible values: [off, error, warn, info, debug, trace] + pub log_level: LevelFilter, + /// Expected payment amount for the random activity generator + pub expected_pmt_amt: u64, + /// Multiplier of the overall network capacity used by the random activity generator + pub capacity_multiplier: f64, + /// Do not create an output file containing the simulations results + pub no_results: bool, + /// Duration after which results are logged + pub log_interval: u64, +} + +impl<'de> Deserialize<'de> for SimulationConfig { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct SimulationConfigHelper { + data_dir: PathBuf, + sim_file: PathBuf, + total_time: Option, + print_batch_size: u32, + log_level: String, + expected_pmt_amt: u64, + capacity_multiplier: f64, + no_results: bool, + log_interval: u64, + } + + let helper: SimulationConfigHelper = Deserialize::deserialize(deserializer)?; + + let log_level = + LevelFilter::from_str(&helper.log_level).map_err(serde::de::Error::custom)?; + + Ok(SimulationConfig { + data_dir: helper.data_dir, + sim_file: helper.sim_file, + total_time: helper.total_time, + print_batch_size: helper.print_batch_size, + log_level, + expected_pmt_amt: helper.expected_pmt_amt, + capacity_multiplier: helper.capacity_multiplier, + no_results: helper.no_results, + log_interval: helper.log_interval, + }) + } +} + +impl SimulationConfig { + /// The default simulation configuration file with default simulation values + pub const DEFAULT_SIM_CONFIG_FILE: &'static str = "conf.json"; + + /// Loads the simulation configuration from a path and overwrites loaded values with + /// CLI arguments + pub fn load( + path: &PathBuf, + Cli { + data_dir, + sim_file, + total_time, + print_batch_size, + log_level, + expected_pmt_amt, + capacity_multiplier, + no_results, + }: &Cli, + ) -> anyhow::Result { + // 1. Load simulation config from specified path + let mut sim_conf = serde_json::from_str::( + &std::fs::read_to_string(&path) + .context(format!("Failed to read content of {:?} to string", path))?, + ) + .context("Failed to deserialize configuration string into SimulationConfig")?; + + // 2. Override config values with CLI arguments (if passed) + if let Some(data_directory) = data_dir { + log::info!( + "SimulatinConfig::data_dir {} overwritten by CLI argument {}", + sim_conf.data_dir.display(), + data_directory.display() + ); + sim_conf.data_dir = data_directory.to_owned(); + } + + if let Some(simulation_file) = sim_file { + log::info!( + "SimulatinConfig::sim_file {} overwritten by CLI argument {}", + sim_conf.sim_file.display(), + simulation_file.display() + ); + sim_conf.sim_file = simulation_file.to_owned(); + } + + if let Some(total_simulation_time) = total_time { + log::info!( + "SimulatinConfig::total_time {:?} overwritten by CLI argument {}", + sim_conf.total_time, + total_simulation_time + ); + sim_conf.total_time = Some(*total_simulation_time); + } + + if let Some(batch_size_to_print) = print_batch_size { + log::info!( + "SimulatinConfig::print_batch_size {:?} overwritten by CLI argument {}", + sim_conf.print_batch_size, + batch_size_to_print + ); + sim_conf.print_batch_size = *batch_size_to_print; + } + + if let Some(log_level_filter) = log_level { + log::info!( + "SimulatinConfig::log_level {:?} overwritten by CLI argument {}", + sim_conf.log_level, + log_level_filter + ); + sim_conf.log_level = *log_level_filter; + } + + if let Some(expected_payment_amount) = expected_pmt_amt { + log::info!( + "SimulatinConfig::expected_pmt_amt {:?} overwritten by CLI argument {}", + sim_conf.expected_pmt_amt, + expected_payment_amount + ); + sim_conf.expected_pmt_amt = *expected_payment_amount; + } + + if let Some(capacity_multiplier) = capacity_multiplier { + log::info!( + "SimulatinConfig::capacity_multiplier {:?} overwritten by CLI argument {}", + sim_conf.capacity_multiplier, + capacity_multiplier + ); + sim_conf.capacity_multiplier = *capacity_multiplier; + } + + if let Some(results) = no_results { + log::info!( + "SimulatinConfig::no_results {:?} overwritten by CLI argument {}", + sim_conf.no_results, + results + ); + sim_conf.no_results = *results; + } + + Ok(sim_conf) + } +} + +#[cfg(test)] +mod test { + use std::{path::PathBuf, str::FromStr}; + + use crate::Cli; + + use super::SimulationConfig; + + #[test] + fn replace_config_with_cli_args() { + let cli = Cli{ + data_dir: Some(PathBuf::from_str("data").expect("Failed to create test data directory")), + sim_file: Some(PathBuf::from_str("sim.json").expect("Failed to create test simulation file")), + total_time: Some(60), + print_batch_size: Some(10), + log_level: Some(log::LevelFilter::Debug), + expected_pmt_amt: Some(500), + capacity_multiplier: Some(3.142), + no_results: Some(true) + }; + + let conf = SimulationConfig::load( + &PathBuf::from("../conf.json"), + &cli + ) + .expect("Failed to create simulation config"); + + assert_eq!(Some(conf.data_dir), cli.data_dir); + assert_eq!(Some(conf.sim_file), cli.sim_file); + assert_eq!(conf.total_time, cli.total_time); + assert_eq!(Some(conf.print_batch_size), cli.print_batch_size); + assert_eq!(Some(conf.log_level), cli.log_level); + assert_eq!(Some(conf.expected_pmt_amt), cli.expected_pmt_amt); + assert_eq!(Some(conf.capacity_multiplier), cli.capacity_multiplier); + assert_eq!(Some(conf.no_results), cli.no_results); + + } +} \ No newline at end of file diff --git a/sim-cli/src/main.rs b/sim-cli/src/main.rs index ac923ae9..35879dd6 100644 --- a/sim-cli/src/main.rs +++ b/sim-cli/src/main.rs @@ -1,4 +1,7 @@ +mod config; + use bitcoin::secp256k1::PublicKey; +use config::SimulationConfig; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; @@ -14,21 +17,6 @@ use sim_lib::{ }; use simple_logger::SimpleLogger; -/// The default directory where the simulation files are stored and where the results will be written to. -pub const DEFAULT_DATA_DIR: &str = "."; - -/// The default simulation file to be used by the simulator. -pub const DEFAULT_SIM_FILE: &str = "sim.json"; - -/// The default expected payment amount for the simulation, around ~$10 at the time of writing. -pub const EXPECTED_PAYMENT_AMOUNT: u64 = 3_800_000; - -/// The number of times over each node in the network sends its total deployed capacity in a calendar month. -pub const ACTIVITY_MULTIPLIER: f64 = 2.0; - -/// Default batch size to flush result data to disk -const DEFAULT_PRINT_BATCH_SIZE: u32 = 500; - /// Deserializes a f64 as long as it is positive and greater than 0. fn deserialize_f64_greater_than_zero(x: String) -> Result { match x.parse::() { @@ -49,45 +37,49 @@ fn deserialize_f64_greater_than_zero(x: String) -> Result { #[command(version, about)] struct Cli { /// Path to a directory containing simulation files, and where simulation results will be stored - #[clap(long, short, default_value = DEFAULT_DATA_DIR)] - data_dir: PathBuf, + #[clap(long, short)] + data_dir: Option, /// Path to the simulation file to be used by the simulator /// This can either be an absolute path, or relative path with respect to data_dir - #[clap(long, short, default_value = DEFAULT_SIM_FILE)] - sim_file: PathBuf, + #[clap(long, short)] + sim_file: Option, /// Total time the simulator will be running #[clap(long, short)] total_time: Option, /// Number of activity results to batch together before printing to csv file [min: 1] - #[clap(long, short, default_value_t = DEFAULT_PRINT_BATCH_SIZE, value_parser = clap::builder::RangedU64ValueParser::::new().range(1..u32::MAX as u64))] - print_batch_size: u32, + #[clap(long, short, value_parser = clap::builder::RangedU64ValueParser::::new().range(1..u32::MAX as u64))] + print_batch_size: Option, /// Level of verbosity of the messages displayed by the simulator. /// Possible values: [off, error, warn, info, debug, trace] - #[clap(long, short, verbatim_doc_comment, default_value = "info")] - log_level: LevelFilter, + #[clap(long, short, verbatim_doc_comment)] + log_level: Option, /// Expected payment amount for the random activity generator - #[clap(long, short, default_value_t = EXPECTED_PAYMENT_AMOUNT, value_parser = clap::builder::RangedU64ValueParser::::new().range(1..u64::MAX))] - expected_pmt_amt: u64, + #[clap(long, short, value_parser = clap::builder::RangedU64ValueParser::::new().range(1..u64::MAX))] + expected_pmt_amt: Option, /// Multiplier of the overall network capacity used by the random activity generator - #[clap(long, short, default_value_t = ACTIVITY_MULTIPLIER, value_parser = clap::builder::StringValueParser::new().try_map(deserialize_f64_greater_than_zero))] - capacity_multiplier: f64, + #[clap(long, short, value_parser = clap::builder::StringValueParser::new().try_map(deserialize_f64_greater_than_zero))] + capacity_multiplier: Option, /// Do not create an output file containing the simulations results - #[clap(long, default_value_t = false)] - no_results: bool, + #[clap(long)] + no_results: Option, } #[tokio::main] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); + let conf = SimulationConfig::load( + &PathBuf::from(SimulationConfig::DEFAULT_SIM_CONFIG_FILE), + &cli, + )?; SimpleLogger::new() .with_level(LevelFilter::Warn) - .with_module_level("sim_lib", cli.log_level) - .with_module_level("sim_cli", cli.log_level) + .with_module_level("sim_lib", conf.log_level) + .with_module_level("sim_cli", conf.log_level) .init() .unwrap(); - let sim_path = read_sim_path(cli.data_dir.clone(), cli.sim_file).await?; + let sim_path = read_sim_path(conf.data_dir.clone(), conf.sim_file).await?; let SimParams { nodes, activity } = serde_json::from_str(&std::fs::read_to_string(sim_path)?) .map_err(|e| anyhow!("Could not deserialize node connection data or activity description from simulation file (line {}, col {}).", e.line(), e.column()))?; @@ -190,10 +182,10 @@ async fn main() -> anyhow::Result<()> { }); } - let write_results = if !cli.no_results { + let write_results = if !conf.no_results { Some(WriteResults { - results_dir: mkdir(cli.data_dir.join("results")).await?, - batch_size: cli.print_batch_size, + results_dir: mkdir(conf.data_dir.join("results")).await?, + batch_size: conf.print_batch_size, }) } else { None @@ -202,10 +194,11 @@ async fn main() -> anyhow::Result<()> { let sim = Simulation::new( clients, validated_activities, - cli.total_time, - cli.expected_pmt_amt, - cli.capacity_multiplier, + conf.total_time, + conf.expected_pmt_amt, + conf.capacity_multiplier, write_results, + conf.log_interval, ); let sim2 = sim.clone();