Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions dash-spv/src/test_utils/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::net::SocketAddr;
use tempfile::TempDir;
use tracing::info;

use super::fs_helpers::{copy_dir, retain_test_dir};
use super::fs_helpers::{copy_dir, retain_test_dir, RetainOnPanic};
use super::node::TestChain;
use super::{DashCoreConfig, DashCoreNode, WalletFile};

Expand Down Expand Up @@ -53,6 +53,8 @@ impl DashdTestContext {
async fn create(mut config: DashCoreConfig) -> Self {
let datadir = TempDir::new().expect("failed to create temp dir");
copy_dir(&config.datadir, datadir.path()).expect("failed to copy datadir");
// Stale fixture locks are cleared in DashCoreNode::start (covers all
// callers, including masternode harnesses).
config.datadir = datadir.path().to_path_buf();
config.wallet = "wallet".to_string();

Expand All @@ -62,14 +64,22 @@ impl DashdTestContext {
wallet.wallet_name, wallet.transaction_count, wallet.utxo_count, wallet.balance
);

// retain_guard is declared before node so reverse-declaration drop order
// shuts dashd down (DashCoreNode::drop / stop_and_wait) before
// RetainOnPanic copies the datadir on post-start panics.
// start() failures retain via fail_startup instead, so the guard is
// installed only after start returns.
let retain_guard;
let mut node = DashCoreNode::with_config(config);
let addr = node.start().await;
info!("DashCoreNode started at {}", addr);
retain_guard = RetainOnPanic::new(datadir.path(), "dashd-startup");

// Load a separate wallet for mining so coinbase rewards don't pollute
// the test wallet's address space (the "wallet" wallet and SPV wallet
// share the same mnemonic).
node.ensure_wallet("default");
// share the same mnemonic). The fixture already ships this wallet on
// disk — load only; never create.
node.load_wallet("default");
info!("Mining wallet 'default' ready");

let initial_height = node.get_block_count();
Expand All @@ -80,6 +90,7 @@ impl DashdTestContext {
info!("RPC miner not available (tests requiring block generation will be skipped)");
}

retain_guard.defuse();
DashdTestContext {
node,
addr,
Expand All @@ -94,6 +105,7 @@ impl DashdTestContext {
impl Drop for DashdTestContext {
fn drop(&mut self) {
let label = format!("dashd-{}", self.addr.port());
self.node.stop_and_wait();
retain_test_dir(self.datadir.path(), &label);
}
}
132 changes: 132 additions & 0 deletions dash-spv/src/test_utils/fs_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,58 @@ pub(super) fn copy_dir(src: &Path, dst: &Path) -> io::Result<()> {
Ok(())
}

/// Remove runtime lock files that must not survive a datadir copy.
///
/// The regtest fixtures are snapshots of a previously running node, so they
/// may contain `regtest/.lock` and per-wallet `.walletlock` files. A live
/// dashd refuses to start (or fails wallet load) when those are present.
pub(super) fn clear_stale_runtime_locks(datadir: &Path) -> io::Result<()> {
let regtest = datadir.join("regtest");
remove_if_exists(&regtest.join(".lock"))?;
// Legacy single-wallet layout stores the lock at regtest/.walletlock.
remove_if_exists(&regtest.join(".walletlock"))?;

// Named wallet directories may sit under regtest/<name>/ or regtest/wallets/<name>/.
let wallets_root = regtest.join("wallets");
for wallet_root in [&regtest, &wallets_root] {
let entries = match fs::read_dir(wallet_root) {
Ok(entries) => entries,
Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
Err(e) => {
return Err(io::Error::new(
e.kind(),
format!("failed to read wallet root {}: {}", wallet_root.display(), e),
));
}
};
for entry in entries {
let entry = entry.map_err(|e| {
io::Error::new(
e.kind(),
format!("failed to read entry in {}: {}", wallet_root.display(), e),
)
})?;
let path = entry.path();
if entry.file_type()?.is_dir() {
remove_if_exists(&path.join(".walletlock"))?;
}
}
}

Ok(())
}

fn remove_if_exists(path: &Path) -> io::Result<()> {
match fs::remove_file(path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(io::Error::new(
e.kind(),
format!("failed to remove stale lock {}: {}", path.display(), e),
)),
}
}

/// When `DASHD_TEST_RETAIN_DIR` is set, copy `src` to a test-named
/// subdirectory for post-mortem inspection.
///
Expand All @@ -33,6 +85,14 @@ pub fn retain_test_dir(src: &Path, label: &str) {
return;
}

retain_test_dir_now(src, label);
}

/// Unconditionally retain `src` when `DASHD_TEST_RETAIN_DIR` is set.
///
/// Use this before panicking during setup that has not yet constructed a type
/// whose `Drop` impl calls [`retain_test_dir`].
pub(super) fn retain_test_dir_now(src: &Path, label: &str) {
let Ok(retain_dir) = std::env::var("DASHD_TEST_RETAIN_DIR") else {
return;
};
Expand All @@ -48,3 +108,75 @@ pub fn retain_test_dir(src: &Path, label: &str) {
eprintln!("Test data retained at: {}", dest.display());
}
}

/// Retains `path` on panic drop when `DASHD_TEST_RETAIN_DIR` is set.
///
/// Used while constructing [`super::DashdTestContext`] so startup failures
/// still leave dashd logs for CI artifacts.
pub(super) struct RetainOnPanic {
path: PathBuf,
label: String,
}

impl RetainOnPanic {
pub(super) fn new(path: impl Into<PathBuf>, label: impl Into<String>) -> Self {
Self {
path: path.into(),
label: label.into(),
}
}

pub(super) fn defuse(self) {
std::mem::forget(self);
}
}

impl Drop for RetainOnPanic {
fn drop(&mut self) {
if std::thread::panicking() {
// Already know we are panicking; skip retain_test_dir's re-check.
retain_test_dir_now(&self.path, &self.label);
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;

#[test]
fn remove_if_exists_treats_missing_file_as_success() {
let tmp = TempDir::new().unwrap();
remove_if_exists(&tmp.path().join("missing.lock")).unwrap();
}

#[test]
fn remove_if_exists_propagates_removal_failures() {
let tmp = TempDir::new().unwrap();
let lock_path = tmp.path().join(".lock");
fs::create_dir(&lock_path).unwrap();

let err = remove_if_exists(&lock_path).unwrap_err();

assert_ne!(err.kind(), io::ErrorKind::NotFound);
}

#[test]
fn clear_stale_runtime_locks_treats_missing_roots_as_success() {
let tmp = TempDir::new().unwrap();
clear_stale_runtime_locks(tmp.path()).unwrap();
}

#[test]
fn clear_stale_runtime_locks_propagates_directory_read_failures() {
let tmp = TempDir::new().unwrap();
let regtest = tmp.path().join("regtest");
fs::create_dir(&regtest).unwrap();
fs::write(regtest.join("wallets"), b"not a directory").unwrap();

let err = clear_stale_runtime_locks(tmp.path()).unwrap_err();

assert_ne!(err.kind(), io::ErrorKind::NotFound);
}
}
Loading
Loading