use std::num::{NonZeroU64, NonZeroUsize}; use lb_framework::{ configs::{ deployment::{DeploymentBuilder, TopologyConfig}, wallet::{WalletConfig, wallet_config_for_users}, }, internal::{DeploymentPlan, apply_wallet_config_to_deployment}, }; pub use testing_framework_core::scenario::ObservabilityBuilderExt; use testing_framework_core::{ scenario::{NodeControlScenarioBuilder, ObservabilityScenarioBuilder}, topology::{DeploymentProvider, DeploymentSeed, DynTopologyError}, }; use tracing::warn; use crate::LbcExtEnv; pub type ScenarioBuilder = testing_framework_core::scenario::ScenarioBuilder; pub type ScenarioBuilderWith = testing_framework_core::scenario::CoreBuilder; pub trait CoreBuilderExt: Sized { fn deployment_with(f: impl FnOnce(DeploymentBuilder) -> DeploymentBuilder) -> Self; fn with_wallet_config(self, wallet: WalletConfig) -> Self; fn wallets(self, users: usize) -> Self; } pub trait ScenarioBuilderExt: Sized { fn transactions(self) -> TransactionFlowBuilder; fn transactions_with( self, f: impl FnOnce(TransactionFlowBuilder) -> TransactionFlowBuilder, ) -> Self; fn expect_consensus_liveness(self) -> Self; fn initialize_wallet(self, total_funds: u64, users: usize) -> Self; } impl CoreBuilderExt for ScenarioBuilder { fn deployment_with(f: impl FnOnce(DeploymentBuilder) -> DeploymentBuilder) -> Self { let topology = f(DeploymentBuilder::new(TopologyConfig::empty())); ScenarioBuilder::new(Box::new(topology)) } fn with_wallet_config(self, wallet: WalletConfig) -> Self { self.map_deployment_provider(|provider| { Box::new(WalletConfigProvider { inner: provider, wallet, }) }) } fn wallets(self, users: usize) -> Self { with_wallets_or_warn(self, users, CoreBuilderExt::with_wallet_config) } } impl CoreBuilderExt for NodeControlScenarioBuilder { fn deployment_with(f: impl FnOnce(DeploymentBuilder) -> DeploymentBuilder) -> Self { ScenarioBuilder::deployment_with(f).enable_node_control() } fn with_wallet_config(self, wallet: WalletConfig) -> Self { self.map_deployment_provider(|provider| { Box::new(WalletConfigProvider { inner: provider, wallet, }) }) } fn wallets(self, users: usize) -> Self { with_wallets_or_warn(self, users, CoreBuilderExt::with_wallet_config) } } impl CoreBuilderExt for ObservabilityScenarioBuilder { fn deployment_with(f: impl FnOnce(DeploymentBuilder) -> DeploymentBuilder) -> Self { ScenarioBuilder::deployment_with(f).enable_observability() } fn with_wallet_config(self, wallet: WalletConfig) -> Self { self.map_deployment_provider(|provider| { Box::new(WalletConfigProvider { inner: provider, wallet, }) }) } fn wallets(self, users: usize) -> Self { with_wallets_or_warn(self, users, CoreBuilderExt::with_wallet_config) } } impl ScenarioBuilderExt for B where B: CoreBuilderExt + testing_framework_core::scenario::CoreBuilderExt + Sized, { fn transactions(self) -> TransactionFlowBuilder { TransactionFlowBuilder { builder: self, rate: NonZeroU64::MIN, users: None, } } fn transactions_with( self, f: impl FnOnce(TransactionFlowBuilder) -> TransactionFlowBuilder, ) -> Self { f(self.transactions()).apply() } fn expect_consensus_liveness(self) -> Self { self.with_expectation(lb_framework::workloads::ConsensusLiveness::::default()) } fn initialize_wallet(self, total_funds: u64, users: usize) -> Self { let Some(user_count) = NonZeroUsize::new(users) else { warn!( users, "wallet user count must be non-zero; ignoring initialize_wallet" ); return self; }; match WalletConfig::uniform(total_funds, user_count) { Ok(wallet) => self.with_wallet_config(wallet), Err(error) => { warn!( users, total_funds, error = %error, "invalid initialize_wallet input; ignoring initialize_wallet" ); self } } } } pub struct TransactionFlowBuilder { builder: B, rate: NonZeroU64, users: Option, } impl TransactionFlowBuilder where B: testing_framework_core::scenario::CoreBuilderExt + Sized, { pub fn rate(mut self, rate: u64) -> Self { match NonZeroU64::new(rate) { Some(rate) => self.rate = rate, None => warn!( rate, "transaction rate must be non-zero; keeping previous rate" ), } self } pub fn users(mut self, users: usize) -> Self { match NonZeroUsize::new(users) { Some(value) => self.users = Some(value), None => warn!( users, "transaction user count must be non-zero; keeping previous setting" ), } self } pub fn apply(self) -> B { let workload = lb_framework::workloads::transaction::Workload::::new(self.rate) .with_user_limit(self.users); self.builder.with_workload(workload) } } struct WalletConfigProvider { inner: Box>, wallet: WalletConfig, } impl DeploymentProvider for WalletConfigProvider { fn build(&self, seed: Option<&DeploymentSeed>) -> Result { let mut deployment = self.inner.build(seed)?; apply_wallet_config_to_deployment(&mut deployment, &self.wallet); Ok(deployment) } } fn with_wallets_or_warn(builder: B, users: usize, apply: impl FnOnce(B, WalletConfig) -> B) -> B where B: CoreBuilderExt, { match wallet_config_for_users(users) { Ok(wallet) => apply(builder, wallet), Err(error) => { warn!(users, error = %error, "invalid wallets input; ignoring wallets"); builder } } }