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
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ permissions:
env:
DASHVERSION: "23.1.0"
TEST_DATA_REPO: "dashpay/regtest-blockchain"
TEST_DATA_VERSION: "v0.0.2"
TEST_DATA_VERSION: "v0.0.3"

jobs:
test:
Expand Down
44 changes: 26 additions & 18 deletions contrib/setup-dashd.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"""Cross-platform setup script for dashd and test blockchain data.

Downloads the Dash Core binary and regtest test data for integration tests.
Outputs DASHD_PATH and DASHD_DATADIR lines suitable for appending to GITHUB_ENV
Outputs DASHD_PATH and DASHD_TEST_DATA lines suitable for appending to GITHUB_ENV
or evaluating in a shell.

Environment variables:
DASHVERSION - Dash Core version (default: 23.1.0)
TEST_DATA_VERSION - Test data release version (default: v0.0.2)
TEST_DATA_VERSION - Test data release version (default: v0.0.3)
TEST_DATA_REPO - GitHub repo for test data (default: dashpay/regtest-blockchain)
CACHE_DIR - Cache directory (default: ~/.rust-dashcore-test)
"""
Expand All @@ -22,7 +22,7 @@

# Keep these defaults in sync with .github/workflows/build-and-test.yml
DASHVERSION = os.environ.get("DASHVERSION", "23.1.0")
TEST_DATA_VERSION = os.environ.get("TEST_DATA_VERSION", "v0.0.2")
TEST_DATA_VERSION = os.environ.get("TEST_DATA_VERSION", "v0.0.3")
TEST_DATA_REPO = os.environ.get("TEST_DATA_REPO", "dashpay/regtest-blockchain")


Expand Down Expand Up @@ -115,23 +115,30 @@ def setup_dashd(cache_dir):
return dashd_bin


def setup_test_data(cache_dir):
"""Download and extract test blockchain data. Returns the datadir path."""
test_data_dir = os.path.join(
cache_dir, f"regtest-blockchain-{TEST_DATA_VERSION}", "regtest-40000"
)
VARIANTS = ["regtest-40000", "regtest-200"]


def setup_test_data(cache_dir, variant):
"""Download and extract a single test blockchain variant.

Args:
cache_dir: Root cache directory for all test assets.
variant: Directory name of the test data (e.g. "regtest-40000" or "regtest-200").
"""
parent_dir = os.path.join(cache_dir, f"regtest-blockchain-{TEST_DATA_VERSION}")
test_data_dir = os.path.join(parent_dir, variant)
blocks_dir = os.path.join(test_data_dir, "regtest", "blocks")

if os.path.isdir(blocks_dir):
log(f"Test blockchain data {TEST_DATA_VERSION} already available")
return test_data_dir
log(f"Test blockchain data {variant} ({TEST_DATA_VERSION}) already available")
return

log(f"Downloading test blockchain data {TEST_DATA_VERSION}...")
parent_dir = os.path.join(cache_dir, f"regtest-blockchain-{TEST_DATA_VERSION}")
log(f"Downloading test blockchain data {variant} ({TEST_DATA_VERSION})...")
os.makedirs(parent_dir, exist_ok=True)

archive_path = os.path.join(cache_dir, "regtest-40000.tar.gz")
url = f"https://github.com/{TEST_DATA_REPO}/releases/download/{TEST_DATA_VERSION}/regtest-40000.tar.gz"
archive_name = f"{variant}.tar.gz"
archive_path = os.path.join(cache_dir, archive_name)
url = f"https://github.com/{TEST_DATA_REPO}/releases/download/{TEST_DATA_VERSION}/{archive_name}"
download(url, archive_path)
extract(archive_path, parent_dir)
os.remove(archive_path)
Expand All @@ -141,19 +148,20 @@ def setup_test_data(cache_dir):

log(f"Downloaded test data to {test_data_dir}")

return test_data_dir


def main():
cache_dir = get_cache_dir()
os.makedirs(cache_dir, exist_ok=True)

dashd_path = setup_dashd(cache_dir)
datadir = setup_test_data(cache_dir)
for variant in VARIANTS:
setup_test_data(cache_dir, variant)

datadir = os.path.join(cache_dir, f"regtest-blockchain-{TEST_DATA_VERSION}")

# Output lines for GITHUB_ENV or shell eval
print(f"DASHD_PATH={dashd_path}")
print(f"DASHD_DATADIR={datadir}")
print(f"DASHD_TEST_DATA={datadir}")


if __name__ == "__main__":
Expand Down
4 changes: 2 additions & 2 deletions dash-spv-ffi/tests/dashd_sync/tests_basic.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use std::collections::HashSet;
use std::sync::atomic::Ordering;

use dash_spv::test_utils::DashdTestContext;
use dash_spv::test_utils::{DashdTestContext, TestChain};

use super::context::FFITestContext;

#[test]
fn test_wallet_sync_via_ffi() {
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
let Some(dashd) = rt.block_on(DashdTestContext::new()) else {
let Some(dashd) = rt.block_on(DashdTestContext::new(TestChain::Full)) else {
eprintln!("Skipping test (dashd context unavailable)");
return;
};
Expand Down
8 changes: 5 additions & 3 deletions dash-spv-ffi/tests/dashd_sync/tests_callback.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::sync::atomic::Ordering;
use std::time::Duration;

use dash_spv::test_utils::DashdTestContext;
use dash_spv::test_utils::{DashdTestContext, TestChain};
use dashcore::hashes::Hash;
use dashcore::Amount;

Expand All @@ -10,7 +10,9 @@ use super::context::FFITestContext;
#[test]
fn test_all_callbacks_during_sync() {
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
let Some(dashd) = rt.block_on(DashdTestContext::new()) else {
// TODO: This should doesn't need a full chain but its currently flaky with the minimal chain
// will be fixed once the flakiness is resolved.
let Some(dashd) = rt.block_on(DashdTestContext::new(TestChain::Full)) else {
return;
};

Expand Down Expand Up @@ -231,7 +233,7 @@ fn test_all_callbacks_during_sync() {
#[test]
fn test_callbacks_post_sync_transactions_and_disconnect() {
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
let Some(dashd) = rt.block_on(DashdTestContext::new()) else {
let Some(dashd) = rt.block_on(DashdTestContext::new(TestChain::Minimal)) else {
return;
};
if !dashd.supports_mining {
Expand Down
4 changes: 2 additions & 2 deletions dash-spv-ffi/tests/dashd_sync/tests_restart.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use std::sync::atomic::Ordering;

use dash_spv::test_utils::DashdTestContext;
use dash_spv::test_utils::{DashdTestContext, TestChain};

use super::context::FFITestContext;

/// Verify FFI client restart preserves consistent state across stop/recreate cycles.
#[test]
fn test_ffi_restart_consistency() {
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
let Some(dashd) = rt.block_on(DashdTestContext::new()) else {
let Some(dashd) = rt.block_on(DashdTestContext::new(TestChain::Full)) else {
eprintln!("Skipping test (dashd context unavailable)");
return;
};
Expand Down
8 changes: 4 additions & 4 deletions dash-spv-ffi/tests/dashd_sync/tests_transaction.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::sync::atomic::Ordering;

use dash_spv::test_utils::DashdTestContext;
use dash_spv::test_utils::{DashdTestContext, TestChain};
use dashcore::hashes::Hash;
use dashcore::Amount;

Expand All @@ -13,7 +13,7 @@ use super::context::FFITestContext;
#[test]
fn test_ffi_sync_then_generate_blocks() {
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
let Some(dashd) = rt.block_on(DashdTestContext::new()) else {
let Some(dashd) = rt.block_on(DashdTestContext::new(TestChain::Minimal)) else {
eprintln!("Skipping test (dashd context unavailable)");
return;
};
Expand Down Expand Up @@ -124,7 +124,7 @@ fn test_ffi_sync_then_generate_blocks() {
#[test]
fn test_ffi_multiple_transactions_in_single_block() {
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
let Some(dashd) = rt.block_on(DashdTestContext::new()) else {
let Some(dashd) = rt.block_on(DashdTestContext::new(TestChain::Minimal)) else {
eprintln!("Skipping test (dashd context unavailable)");
return;
};
Expand Down Expand Up @@ -208,7 +208,7 @@ fn test_ffi_multiple_transactions_in_single_block() {
#[test]
fn test_ffi_multiple_transactions_across_blocks() {
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
let Some(dashd) = rt.block_on(DashdTestContext::new()) else {
let Some(dashd) = rt.block_on(DashdTestContext::new(TestChain::Minimal)) else {
eprintln!("Skipping test (dashd context unavailable)");
return;
};
Expand Down
25 changes: 17 additions & 8 deletions dash-spv/src/test_utils/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use tempfile::TempDir;
use tracing::info;

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

/// Shared test infrastructure for dashd integration tests.
Expand All @@ -32,17 +33,24 @@ pub struct DashdTestContext {
}

impl DashdTestContext {
/// Create a new dashd test context.
/// Create a new dashd test context for the given chain variant.
///
/// Returns `None` if `SKIP_DASHD_TESTS` is set. Panics if required env vars
/// are missing or if dashd fails to start.
pub async fn new() -> Option<Self> {
/// Returns `None` if `SKIP_DASHD_TESTS` is set. Panics if the variant
/// directory is not available under `DASHD_TEST_DATA` or if dashd fails
/// to start.
pub async fn new(chain: TestChain) -> Option<Self> {
if std::env::var("SKIP_DASHD_TESTS").is_ok() {
eprintln!("Skipping dashd integration test (SKIP_DASHD_TESTS is set)");
return None;
}

let mut config = DashCoreConfig::from_env();
let config = DashCoreConfig::from_env(chain)
.unwrap_or_else(|| panic!("DASHD_TEST_DATA/{} not found", chain.variant_dir()));
Some(Self::create(config).await)
}

/// Shared initialization: copies the datadir, starts dashd, loads wallets.
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");
config.datadir = datadir.path().to_path_buf();
Expand Down Expand Up @@ -72,19 +80,20 @@ impl DashdTestContext {
info!("RPC miner not available (tests requiring block generation will be skipped)");
}

Some(DashdTestContext {
DashdTestContext {
node,
addr,
initial_height,
wallet,
supports_mining,
datadir,
})
}
}
}

impl Drop for DashdTestContext {
fn drop(&mut self) {
retain_test_dir(self.datadir.path(), "dashd");
let label = format!("dashd-{}", self.addr.port());
retain_test_dir(self.datadir.path(), &label);
}
}
2 changes: 1 addition & 1 deletion dash-spv/src/test_utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ pub const SYNC_TIMEOUT: Duration = Duration::from_secs(180);
pub use context::DashdTestContext;
pub use fs_helpers::retain_test_dir;
pub use network::{test_socket_address, MockNetworkManager};
pub use node::{DashCoreNode, WalletFile};
pub use node::{DashCoreNode, TestChain, WalletFile};

pub(crate) use node::DashCoreConfig;
40 changes: 32 additions & 8 deletions dash-spv/src/test_utils/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ fn find_available_port() -> u16 {
panic!("failed to find an available port after {} attempts", MAX_PORT_ATTEMPTS);
}

/// Selects which pre-built regtest blockchain to use for integration tests.
#[derive(Debug, Clone, Copy)]
pub enum TestChain {
/// Full 40,000-block regtest chain (wallet integration tests).
Full,
/// Minimal 200-block regtest chain (faster tests).
Minimal,
}

impl TestChain {
pub(crate) fn variant_dir(self) -> &'static str {
match self {
TestChain::Full => "regtest-40000",
TestChain::Minimal => "regtest-200",
}
}
}

/// Configuration for Dash Core node.
pub struct DashCoreConfig {
/// Path to dashd binary
Expand All @@ -46,12 +64,13 @@ pub struct DashCoreConfig {
}

impl DashCoreConfig {
/// Create a config from environment variables with dynamically allocated ports.
/// Create a config for the given test chain variant under `DASHD_TEST_DATA`.
///
/// Reads `DASHD_PATH` and `DASHD_DATADIR`. Panics if either variable
/// is not set or if the dashd binary doesn't exist.
pub fn from_env() -> Self {
let error = "DASHD_PATH and DASHD_DATADIR environment variables are required. \
/// `DASHD_TEST_DATA` points to the root directory containing all variants
/// (e.g. `regtest-40000`, `regtest-200`). Returns `None` if the variant
/// directory doesn't exist. Panics if env vars are missing.
pub fn from_env(chain: TestChain) -> Option<Self> {
let error = "DASHD_PATH and DASHD_TEST_DATA environment variables are required. \
Either run `eval $(python3 contrib/setup-dashd.py)` to set them up, \
or set SKIP_DASHD_TESTS=1 to skip these tests. \
In CI, the setup-dashd step in build-and-test.yml handles this automatically.";
Expand All @@ -63,15 +82,20 @@ impl DashCoreConfig {
dashd_path.display()
);

let datadir = std::env::var("DASHD_DATADIR").ok().map(PathBuf::from).expect(error);
let base_datadir = std::env::var("DASHD_TEST_DATA").ok().map(PathBuf::from).expect(error);
let datadir = base_datadir.join(chain.variant_dir());

Self {
if !datadir.exists() {
return None;
}

Some(Self {
dashd_path,
datadir,
wallet: "default".to_string(),
p2p_port: find_available_port(),
rpc_port: find_available_port(),
}
})
}
}

Expand Down
18 changes: 10 additions & 8 deletions dash-spv/tests/dashd_sync/setup.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use dash_spv::network::NetworkEvent;
use dash_spv::storage::{PeerStorage, PersistentPeerStorage, PersistentStorage};
use dash_spv::test_utils::{retain_test_dir, DashdTestContext};
use dash_spv::test_utils::{retain_test_dir, DashdTestContext, TestChain};
use dash_spv::{
client::{ClientConfig, DashSpvClient},
network::PeerNetworkManager,
Expand Down Expand Up @@ -51,14 +51,18 @@ impl TestContext {
///
/// # Example
/// ```rust
/// if let Some(context) = TestContext::new().await {
/// if let Some(context) = TestContext::new(TestChain::Full).await {
/// // Proceed with using the `context` for testing.
/// } else {
/// eprintln!("Failed to create the test context");
/// }
/// ```
pub(super) async fn new() -> Option<Self> {
// Create storage dir first so we can set up per-test file logging
pub(super) async fn new(chain: TestChain) -> Option<Self> {
let dashd = DashdTestContext::new(chain).await?;
Some(Self::create(dashd))
}

fn create(dashd: DashdTestContext) -> Self {
let storage_dir = TempDir::new().expect("Failed to create temporary directory");
let log_dir = storage_dir.path().join("logs");
let _log_guard = dash_spv::init_logging(dash_spv::LoggingConfig {
Expand All @@ -72,8 +76,6 @@ impl TestContext {
})
.expect("Failed to initialize test logging");

let dashd = DashdTestContext::new().await?;

let client_config = create_test_config(storage_dir.path().to_path_buf(), dashd.addr);

let (wallet, wallet_id) = create_test_wallet(&dashd.wallet.mnemonic, Network::Regtest);
Expand All @@ -85,14 +87,14 @@ impl TestContext {
storage_dir.path().display(),
);

Some(TestContext {
TestContext {
dashd,
storage_dir,
client_config,
wallet,
wallet_id,
_log_guard,
})
}
}
/// Spawns and initializes a new client instance asynchronously.
pub(super) async fn spawn_new_client(&self) -> ClientHandle {
Expand Down
Loading
Loading