fixtures.rs
1 use std::path::PathBuf; 2 use std::sync::Arc; 3 use std::time::Duration; 4 use std::{env, fs}; 5 6 use fedimint_bitcoind::{create_bitcoind, DynBitcoindRpc}; 7 use fedimint_client::module::init::{ 8 ClientModuleInitRegistry, DynClientModuleInit, IClientModuleInit, 9 }; 10 use fedimint_core::config::{ 11 ModuleInitParams, ServerModuleConfigGenParamsRegistry, ServerModuleInitRegistry, 12 }; 13 use fedimint_core::core::{ModuleInstanceId, ModuleKind}; 14 use fedimint_core::envs::BitcoinRpcConfig; 15 use fedimint_core::module::{DynServerModuleInit, IServerModuleInit}; 16 use fedimint_core::runtime::block_in_place; 17 use fedimint_core::task::{MaybeSend, MaybeSync, TaskGroup}; 18 use fedimint_core::util::SafeUrl; 19 use fedimint_logging::TracingSetup; 20 use tempfile::TempDir; 21 22 use crate::btc::mock::FakeBitcoinFactory; 23 use crate::btc::real::RealBitcoinTest; 24 use crate::btc::BitcoinTest; 25 use crate::envs::{ 26 FM_PORT_ESPLORA_ENV, FM_TEST_BITCOIND_RPC_ENV, FM_TEST_DIR_ENV, FM_TEST_USE_REAL_DAEMONS_ENV, 27 }; 28 use crate::federation::{FederationTest, FederationTestBuilder}; 29 use crate::gateway::GatewayTest; 30 use crate::ln::FakeLightningTest; 31 32 /// A default timeout for things happening in tests 33 pub const TIMEOUT: Duration = Duration::from_secs(10); 34 35 /// A tool for easily writing fedimint integration tests 36 pub struct Fixtures { 37 clients: Vec<DynClientModuleInit>, 38 servers: Vec<DynServerModuleInit>, 39 params: ServerModuleConfigGenParamsRegistry, 40 bitcoin_rpc: BitcoinRpcConfig, 41 bitcoin: Arc<dyn BitcoinTest>, 42 dyn_bitcoin_rpc: DynBitcoindRpc, 43 id: ModuleInstanceId, 44 } 45 46 impl Fixtures { 47 pub fn new_primary( 48 client: impl IClientModuleInit + 'static, 49 server: impl IServerModuleInit + MaybeSend + MaybeSync + 'static, 50 params: impl ModuleInitParams, 51 ) -> Self { 52 // Ensure tracing has been set once 53 let _ = TracingSetup::default().init(); 54 let real_testing = Fixtures::is_real_test(); 55 let task_group = TaskGroup::new(); 56 let (dyn_bitcoin_rpc, bitcoin, config): ( 57 DynBitcoindRpc, 58 Arc<dyn BitcoinTest>, 59 BitcoinRpcConfig, 60 ) = if real_testing { 61 let rpc_config = BitcoinRpcConfig::get_defaults_from_env_vars().unwrap(); 62 let dyn_bitcoin_rpc = create_bitcoind(&rpc_config, task_group.make_handle()).unwrap(); 63 let bitcoincore_url = env::var(FM_TEST_BITCOIND_RPC_ENV) 64 .expect("Must have bitcoind RPC defined for real tests") 65 .parse() 66 .expect("Invalid bitcoind RPC URL"); 67 let bitcoin = RealBitcoinTest::new(&bitcoincore_url, dyn_bitcoin_rpc.clone()); 68 (dyn_bitcoin_rpc, Arc::new(bitcoin), rpc_config) 69 } else { 70 let FakeBitcoinFactory { bitcoin, config } = FakeBitcoinFactory::register_new(); 71 let dyn_bitcoin_rpc = DynBitcoindRpc::from(bitcoin.clone()); 72 let bitcoin = Arc::new(bitcoin); 73 (dyn_bitcoin_rpc, bitcoin, config) 74 }; 75 76 Self { 77 clients: vec![], 78 servers: vec![], 79 params: Default::default(), 80 bitcoin_rpc: config, 81 bitcoin, 82 dyn_bitcoin_rpc, 83 id: 0, 84 } 85 .with_module(client, server, params) 86 } 87 88 pub fn is_real_test() -> bool { 89 env::var(FM_TEST_USE_REAL_DAEMONS_ENV) == Ok("1".to_string()) 90 } 91 92 // TODO: Auto-assign instance ids after removing legacy id order 93 /// Add a module to the fed 94 pub fn with_module( 95 mut self, 96 client: impl IClientModuleInit + 'static, 97 server: impl IServerModuleInit + MaybeSend + MaybeSync + 'static, 98 params: impl ModuleInitParams, 99 ) -> Self { 100 self.params 101 .attach_config_gen_params_by_id(self.id, server.module_kind(), params); 102 self.clients.push(DynClientModuleInit::from(client)); 103 self.servers.push(DynServerModuleInit::from(server)); 104 self.id += 1; 105 106 self 107 } 108 109 pub fn with_server_only_module( 110 mut self, 111 server: impl IServerModuleInit + MaybeSend + MaybeSync + 'static, 112 params: impl ModuleInitParams, 113 ) -> Self { 114 self.params 115 .attach_config_gen_params_by_id(self.id, server.module_kind(), params); 116 self.servers.push(DynServerModuleInit::from(server)); 117 self.id += 1; 118 119 self 120 } 121 122 /// Starts a new federation with default number of peers for testing 123 pub async fn new_default_fed(&self) -> FederationTest { 124 let federation_builder = FederationTestBuilder::new( 125 self.params.clone(), 126 ServerModuleInitRegistry::from(self.servers.clone()), 127 ClientModuleInitRegistry::from(self.clients.clone()), 128 ) 129 .await; 130 federation_builder.build().await 131 } 132 133 pub async fn new_fed_builder(&self) -> FederationTestBuilder { 134 FederationTestBuilder::new( 135 self.params.clone(), 136 ServerModuleInitRegistry::from(self.servers.clone()), 137 ClientModuleInitRegistry::from(self.clients.clone()), 138 ) 139 .await 140 } 141 142 /// Starts a new gateway with a given lightning node 143 pub async fn new_gateway( 144 &self, 145 num_route_hints: u32, 146 cli_password: Option<String>, 147 ) -> GatewayTest { 148 // TODO: Make construction easier 149 let server_gens = ServerModuleInitRegistry::from(self.servers.clone()); 150 let module_kinds = self.params.iter_modules().map(|(id, kind, _)| (id, kind)); 151 let decoders = server_gens.available_decoders(module_kinds).unwrap(); 152 let clients = self.clients.clone().into_iter(); 153 154 GatewayTest::new( 155 block_in_place(|| fedimint_portalloc::port_alloc(1)) 156 .expect("Failed to allocate a port range"), 157 cli_password, 158 FakeLightningTest::new(), 159 decoders, 160 ClientModuleInitRegistry::from_iter( 161 clients 162 .filter(|client| { 163 // Remove LN module because the gateway adds one 164 client.to_dyn_common().module_kind() != ModuleKind::from_static_str("ln") 165 }) 166 .filter(|client| { 167 // Remove LN NG module because the gateway adds one 168 client.to_dyn_common().module_kind() != ModuleKind::from_static_str("lnv2") 169 }), 170 ), 171 num_route_hints, 172 ) 173 .await 174 } 175 176 /// Get a server bitcoin RPC config 177 pub fn bitcoin_server(&self) -> BitcoinRpcConfig { 178 self.bitcoin_rpc.clone() 179 } 180 181 /// Get a client bitcoin RPC config 182 // TODO: Right now we only support mocks or esplora, we should support others in 183 // the future 184 pub fn bitcoin_client(&self) -> BitcoinRpcConfig { 185 match Fixtures::is_real_test() { 186 true => BitcoinRpcConfig { 187 kind: "esplora".to_string(), 188 url: SafeUrl::parse(&format!( 189 "http://127.0.0.1:{}/", 190 env::var(FM_PORT_ESPLORA_ENV).unwrap_or(String::from("50002")) 191 )) 192 .expect("Failed to parse default esplora server"), 193 }, 194 false => self.bitcoin_rpc.clone(), 195 } 196 } 197 198 /// Get a test bitcoin fixture 199 pub fn bitcoin(&self) -> Arc<dyn BitcoinTest> { 200 self.bitcoin.clone() 201 } 202 203 pub fn dyn_bitcoin_rpc(&self) -> DynBitcoindRpc { 204 self.dyn_bitcoin_rpc.clone() 205 } 206 } 207 208 /// If `FM_TEST_DIR` is set, use it as a base, otherwise use a tempdir 209 /// 210 /// Callers must hold onto the tempdir until it is no longer needed 211 pub fn test_dir(pathname: &str) -> (PathBuf, Option<TempDir>) { 212 let (parent, maybe_tmp_dir_guard) = match env::var(FM_TEST_DIR_ENV) { 213 Ok(directory) => (directory, None), 214 Err(_) => { 215 let random = format!("test-{}", rand::random::<u64>()); 216 let guard = tempfile::Builder::new().prefix(&random).tempdir().unwrap(); 217 let directory = guard.path().to_str().unwrap().to_owned(); 218 (directory, Some(guard)) 219 } 220 }; 221 let fullpath = PathBuf::from(parent).join(pathname); 222 fs::create_dir_all(fullpath.clone()).expect("Can make dirs"); 223 (fullpath, maybe_tmp_dir_guard) 224 }