Introduce a testutils module with a DummyMinisafe for unit tests

So we can have unit tests without a dummy bitcoind thread, as we
currently do for the startup test.

This commit also implements sanity checks for the two existing commands
using this mechanism.
This commit is contained in:
Antoine Poinsot 2022-08-09 15:18:47 +02:00
parent e510c0a30d
commit d03c469967
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
5 changed files with 179 additions and 18 deletions

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);

124
src/testutils.rs Normal file
View File

@ -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<BlockChainTip>,
}
impl DummyDb {
pub fn new() -> DummyDb {
DummyDb {
curr_index: 0.into(),
curr_tip: None,
}
}
}
impl DatabaseInterface for sync::Arc<sync::RwLock<DummyDb>> {
fn connection(&self) -> Box<dyn DatabaseConnection> {
Box::new(DummyDbConn { db: self.clone() })
}
}
pub struct DummyDbConn {
db: sync::Arc<sync::RwLock<DummyDb>>,
}
impl DatabaseConnection for DummyDbConn {
fn chain_tip(&mut self) -> Option<BlockChainTip> {
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();
}
}