diff --git a/src/bitcoin/mod.rs b/src/bitcoin/mod.rs index 0bf4170d..54406ec3 100644 --- a/src/bitcoin/mod.rs +++ b/src/bitcoin/mod.rs @@ -9,7 +9,7 @@ use std::sync; use miniscript::bitcoin; /// Information about the best block in the chain -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Copy)] pub struct BlockChainTip { pub hash: bitcoin::BlockHash, pub height: i32, diff --git a/src/bitcoin/poller/mod.rs b/src/bitcoin/poller/mod.rs index 68901d61..97a1c22d 100644 --- a/src/bitcoin/poller/mod.rs +++ b/src/bitcoin/poller/mod.rs @@ -38,4 +38,9 @@ impl Poller { self.shutdown.store(true, atomic::Ordering::Relaxed); self.handle.join().expect("The poller loop must not fail"); } + + #[cfg(test)] + pub fn test_stop(&mut self) { + self.shutdown.store(true, atomic::Ordering::Relaxed); + } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 444b6d05..37a81aea 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -64,3 +64,39 @@ pub struct GetInfoResult { pub struct GetAddressResult { pub address: bitcoin::Address, } + +#[cfg(test)] +mod tests { + use super::*; + use crate::testutils::*; + use std::str::FromStr; + + #[test] + fn getinfo() { + let ms = DummyMinisafe::new(); + // We can query getinfo + ms.handle.control.get_info(); + ms.shutdown(); + } + + #[test] + fn getnewaddress() { + let ms = DummyMinisafe::new(); + + let control = &ms.handle.control; + // We can get an address + let addr = control.get_new_address().address; + assert_eq!( + addr, + bitcoin::Address::from_str( + "bc1qgudekhcrejgtlx3yhlvdul7t4q76e5lhm0vtcsndxs6aslh4r9jsqkqhwu" + ) + .unwrap() + ); + // We won't get the same twice. + let addr2 = control.get_new_address().address; + assert_ne!(addr, addr2); + + ms.shutdown(); + } +} diff --git a/src/lib.rs b/src/lib.rs index cac92090..fc425cb0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,8 @@ mod database; pub mod descriptors; #[cfg(feature = "jsonrpc_server")] mod jsonrpc; +#[cfg(test)] +mod testutils; pub use miniscript; @@ -359,6 +361,12 @@ impl DaemonHandle { pub fn shutdown(self) { self.bitcoin_poller.stop(); } + + // We need a shutdown utility that does not move for implementing Drop for the DummyMinisafe + #[cfg(test)] + pub fn test_shutdown(&mut self) { + self.bitcoin_poller.test_stop(); + } } #[cfg(all(test, unix))] @@ -515,6 +523,10 @@ mod tests { stream.flush().unwrap(); } + // TODO: we could move the dummy bitcoind thread stuff to the bitcoind module to test the + // bitcoind interface, and use the DummyMinisafe from testutils to sanity check the startup. + // Note that startup as checked by this unit test is also tested in the functional test + // framework. #[test] fn daemon_startup() { let tmp_dir = env::temp_dir().join(format!( @@ -578,21 +590,7 @@ mod tests { let config = config.clone(); move || { let handle = DaemonHandle::start_default(config).unwrap(); - // TODO: avoid scope creep. We should move the bitcoind-specific checks to the - // bitcoind module, test the startup with a mocked bitcoind interface, and not test - // commands here but in the commands module. - let addr = handle.control.get_new_address().address; - let addr2 = handle.control.get_new_address().address; - assert_eq!( - addr, - bitcoin::Address::from_str( - "bc1qdu9dama0pwc6fd9lj4sqzq4f728y5q2ucqyj55mfzfvuxr268zks7yajm3" - ) - .unwrap() - ); - assert_ne!(addr, addr2); handle.shutdown(); - addr } }); complete_sanity_check(&server); @@ -603,13 +601,11 @@ mod tests { complete_wallet_check(&server, &wo_path); complete_desc_check(&server, desc_str); complete_sync_check(&server); - let addr = daemon_thread.join().unwrap(); + daemon_thread.join().unwrap(); // The datadir is created now, so if we restart it it won't create the wo wallet. let daemon_thread = thread::spawn(move || { let handle = DaemonHandle::start_default(config).unwrap(); - // TODO: avoid scope creep. See above comment. - assert_ne!(handle.control.get_new_address().address, addr); handle.shutdown(); }); complete_sanity_check(&server); diff --git a/src/testutils.rs b/src/testutils.rs new file mode 100644 index 00000000..0d8d5cea --- /dev/null +++ b/src/testutils.rs @@ -0,0 +1,124 @@ +use crate::{ + bitcoin::{BitcoinInterface, BlockChainTip}, + config::{BitcoinConfig, Config}, + database::{DatabaseConnection, DatabaseInterface}, + DaemonControl, DaemonHandle, +}; + +use std::{env, fs, path, process, str::FromStr, sync, thread, time}; + +use miniscript::{ + bitcoin::{self, util::bip32}, + descriptor, +}; + +pub struct DummyBitcoind {} + +impl BitcoinInterface for DummyBitcoind { + fn sync_progress(&self) -> f64 { + 1.0 + } + + fn chain_tip(&self) -> BlockChainTip { + let hash = bitcoin::BlockHash::from_str( + "000000007bc154e0fa7ea32218a72fe2c1bb9f86cf8c9ebf9a715ed27fdb229a", + ) + .unwrap(); + let height = 100; + BlockChainTip { hash, height } + } + + fn is_in_chain(&self, _: &BlockChainTip) -> bool { + // No reorg + true + } +} + +pub struct DummyDb { + curr_index: bip32::ChildNumber, + curr_tip: Option, +} + +impl DummyDb { + pub fn new() -> DummyDb { + DummyDb { + curr_index: 0.into(), + curr_tip: None, + } + } +} + +impl DatabaseInterface for sync::Arc> { + fn connection(&self) -> Box { + Box::new(DummyDbConn { db: self.clone() }) + } +} + +pub struct DummyDbConn { + db: sync::Arc>, +} + +impl DatabaseConnection for DummyDbConn { + fn chain_tip(&mut self) -> Option { + self.db.read().unwrap().curr_tip + } + + fn update_tip(&mut self, tip: &BlockChainTip) { + self.db.write().unwrap().curr_tip = Some(*tip); + } + + fn derivation_index(&mut self) -> bip32::ChildNumber { + self.db.read().unwrap().curr_index + } + + fn update_derivation_index(&mut self, index: bip32::ChildNumber) { + self.db.write().unwrap().curr_index = index; + } +} + +pub struct DummyMinisafe { + tmp_dir: path::PathBuf, + pub handle: DaemonHandle, +} + +impl DummyMinisafe { + pub fn new() -> DummyMinisafe { + let tmp_dir = env::temp_dir().join(format!( + "minisafed-unit-tests-{}-{:?}", + process::id(), + thread::current().id() + )); + fs::create_dir_all(&tmp_dir).unwrap(); + let data_dir: path::PathBuf = [tmp_dir.as_path(), path::Path::new("datadir")] + .iter() + .collect(); + + let network = bitcoin::Network::Bitcoin; + let bitcoin_config = BitcoinConfig { + network, + poll_interval_secs: time::Duration::from_secs(2), + }; + + let owner_key = descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/*").unwrap(); + let heir_key = descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/*").unwrap(); + let desc = crate::descriptors::inheritance_descriptor(owner_key, heir_key, 10_000).unwrap(); + let config = Config { + bitcoin_config, + bitcoind_config: None, + data_dir: Some(data_dir.clone()), + #[cfg(unix)] + daemon: false, + log_level: log::LevelFilter::Debug, + main_descriptor: desc, + }; + + let db = sync::Arc::from(sync::RwLock::from(DummyDb::new())); + let handle = DaemonHandle::start(config, Some(DummyBitcoind {}), Some(db)).unwrap(); + DummyMinisafe { tmp_dir, handle } + } + + pub fn shutdown(self) { + self.handle.shutdown(); + fs::remove_dir_all(&self.tmp_dir).unwrap(); + } +}