use super::anvil::TestAnvil;
use super::testnet::{TestNetwork, TestNetworkConfig, TestNode};
use ant_node::client::XorName;
use evmlib::common::TxHash;
use saorsa_core::P2PNode;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tracing::info;
#[derive(Debug, thiserror::Error)]
pub enum HarnessError {
#[error("Testnet error: {0}")]
Testnet(#[from] super::testnet::TestnetError),
#[error("Anvil error: {0}")]
Anvil(String),
#[error("Node not found: index {0}")]
NodeNotFound(usize),
}
pub type Result<T> = std::result::Result<T, HarnessError>;
#[derive(Debug, Clone)]
pub struct PaymentRecord {
pub chunk_address: XorName,
pub tx_hashes: Vec<TxHash>,
pub timestamp: std::time::SystemTime,
}
#[derive(Debug, Clone, Default)]
pub struct PaymentTracker {
payments: Arc<Mutex<HashMap<XorName, Vec<PaymentRecord>>>>,
}
impl PaymentTracker {
#[must_use]
pub fn new() -> Self {
Self {
payments: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn record_payment(&self, chunk_address: XorName, tx_hashes: Vec<TxHash>) {
let record = PaymentRecord {
chunk_address,
tx_hashes,
timestamp: std::time::SystemTime::now(),
};
let mut payments = self
.payments
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
payments.entry(chunk_address).or_default().push(record);
}
#[must_use]
pub fn payment_count(&self, chunk_address: &XorName) -> usize {
let payments = self
.payments
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
payments.get(chunk_address).map_or(0, Vec::len)
}
#[must_use]
pub fn get_payments(&self, chunk_address: &XorName) -> Vec<PaymentRecord> {
let payments = self
.payments
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
payments.get(chunk_address).cloned().unwrap_or_default()
}
#[must_use]
pub fn unique_chunk_count(&self) -> usize {
let payments = self
.payments
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
payments.len()
}
#[must_use]
pub fn total_payment_count(&self) -> usize {
let payments = self
.payments
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
payments.values().map(Vec::len).sum()
}
#[must_use]
pub fn has_duplicate_payments(&self) -> bool {
let payments = self
.payments
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
payments.values().any(|records| records.len() > 1)
}
#[must_use]
pub fn chunks_with_duplicates(&self) -> Vec<XorName> {
let payments = self
.payments
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
payments
.iter()
.filter(|(_, records)| records.len() > 1)
.map(|(addr, _)| *addr)
.collect()
}
}
pub struct TestHarness {
network: TestNetwork,
anvil: Option<TestAnvil>,
payment_tracker: PaymentTracker,
}
impl TestHarness {
pub async fn setup() -> Result<Self> {
Self::setup_with_config(TestNetworkConfig::default()).await
}
pub async fn setup_with_config(config: TestNetworkConfig) -> Result<Self> {
info!("Setting up test harness with {} nodes", config.node_count);
let mut network = TestNetwork::new(config).await?;
network.start().await?;
Ok(Self {
network,
anvil: None,
payment_tracker: PaymentTracker::new(),
})
}
pub async fn setup_minimal() -> Result<Self> {
Self::setup_with_config(TestNetworkConfig::minimal()).await
}
pub async fn setup_small() -> Result<Self> {
Self::setup_with_config(TestNetworkConfig::small()).await
}
pub async fn setup_with_evm() -> Result<Self> {
Self::setup_with_evm_and_config(TestNetworkConfig::default()).await
}
pub async fn setup_with_payments() -> Result<Self> {
Self::setup_with_evm().await
}
pub async fn setup_with_evm_and_config(config: TestNetworkConfig) -> Result<Self> {
info!(
"Setting up test harness with {} nodes and Anvil EVM",
config.node_count
);
let mut network = TestNetwork::new(config).await?;
network.start().await?;
info!("Warming up DHT routing tables...");
network.warmup_dht().await?;
let anvil = TestAnvil::new()
.await
.map_err(|e| HarnessError::Anvil(format!("Failed to start Anvil: {e}")))?;
Ok(Self {
network,
anvil: Some(anvil),
payment_tracker: PaymentTracker::new(),
})
}
#[must_use]
pub fn payment_tracker(&self) -> &PaymentTracker {
&self.payment_tracker
}
#[must_use]
pub fn network(&self) -> &TestNetwork {
&self.network
}
#[must_use]
pub fn network_mut(&mut self) -> &mut TestNetwork {
&mut self.network
}
#[must_use]
pub fn anvil(&self) -> Option<&TestAnvil> {
self.anvil.as_ref()
}
#[must_use]
pub fn has_evm(&self) -> bool {
self.anvil.is_some()
}
#[must_use]
pub fn node(&self, index: usize) -> Option<Arc<P2PNode>> {
self.network.node(index)?.p2p_node.clone()
}
#[must_use]
pub fn test_node(&self, index: usize) -> Option<&TestNode> {
self.network.node(index)
}
#[must_use]
pub fn test_node_mut(&mut self, index: usize) -> Option<&mut TestNode> {
self.network.node_mut(index)
}
#[allow(clippy::panic)]
pub fn prepopulate_payment_cache_for_peer(
&self,
peer_id: &saorsa_core::identity::PeerId,
address: &XorName,
) {
for node in self.network.nodes() {
if let Some(ref p2p) = node.p2p_node {
if p2p.peer_id() == peer_id {
if let Some(ref protocol) = node.ant_protocol {
protocol.payment_verifier().cache_insert(*address);
}
return;
}
}
}
panic!("prepopulate_payment_cache_for_peer: no running node with peer_id {peer_id}");
}
#[must_use]
pub fn random_node(&self) -> Option<Arc<P2PNode>> {
use rand::seq::SliceRandom;
let regular_nodes: Vec<_> = self
.network
.regular_nodes()
.iter()
.filter(|n| n.p2p_node.is_some())
.collect();
regular_nodes
.choose(&mut rand::thread_rng())
.and_then(|n| n.p2p_node.clone())
}
#[must_use]
pub fn random_bootstrap_node(&self) -> Option<Arc<P2PNode>> {
use rand::seq::SliceRandom;
let bootstrap_nodes: Vec<_> = self
.network
.bootstrap_nodes()
.iter()
.filter(|n| n.p2p_node.is_some())
.collect();
bootstrap_nodes
.choose(&mut rand::thread_rng())
.and_then(|n| n.p2p_node.clone())
}
#[must_use]
pub fn all_nodes(&self) -> Vec<Arc<P2PNode>> {
self.network
.nodes()
.iter()
.filter_map(|n| n.p2p_node.clone())
.collect()
}
#[must_use]
pub fn node_count(&self) -> usize {
self.network.node_count()
}
pub async fn is_ready(&self) -> bool {
self.network.is_ready().await
}
pub async fn total_connections(&self) -> usize {
self.network.total_connections().await
}
pub async fn shutdown_node(&mut self, index: usize) -> Result<()> {
self.network.shutdown_node(index).await?;
Ok(())
}
pub async fn shutdown_nodes(&mut self, indices: &[usize]) -> Result<()> {
self.network.shutdown_nodes(indices).await?;
Ok(())
}
pub async fn running_node_count(&self) -> usize {
self.network.running_node_count().await
}
pub async fn warmup_dht(&self) -> Result<()> {
self.network.warmup_dht().await?;
Ok(())
}
pub async fn add_node(&mut self) -> Result<usize> {
Ok(self.network.add_node().await?)
}
pub async fn teardown(mut self) -> Result<()> {
info!("Tearing down test harness");
self.network.shutdown().await?;
if let Some(mut anvil) = self.anvil.take() {
anvil.shutdown().await;
}
info!("Test harness teardown complete");
Ok(())
}
}
#[macro_export]
macro_rules! with_test_network {
($harness:ident, $body:block) => {{
let $harness = $crate::tests::e2e::TestHarness::setup().await?;
let result: Result<(), Box<dyn std::error::Error>> = async { $body }.await;
$harness.teardown().await?;
result
}};
($harness:ident, $config:expr, $body:block) => {{
let $harness = $crate::tests::e2e::TestHarness::setup_with_config($config).await?;
let result: Result<(), Box<dyn std::error::Error>> = async { $body }.await;
$harness.teardown().await?;
result
}};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_harness_error_display() {
let err = HarnessError::NodeNotFound(5);
assert!(err.to_string().contains('5'));
}
}