#![cfg(feature = "provider")]
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use alloy_chains::NamedChain;
use alloy_provider::Provider;
use semioscan::provider::{
create_http_provider, ChainEndpoint, ProviderConfig, ProviderPoolBuilder,
};
use semioscan::{RpcPolicy, SemioscanConfigBuilder};
use tokio::net::{TcpListener, TcpStream};
async fn spawn_stalled_listener() -> String {
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr: SocketAddr = listener.local_addr().expect("local_addr");
let url = format!("http://{addr}");
let parked: Arc<tokio::sync::Mutex<Vec<TcpStream>>> = Arc::new(tokio::sync::Mutex::new(vec![]));
tokio::spawn({
let parked = Arc::clone(&parked);
async move {
loop {
let Ok((stream, _peer)) = listener.accept().await else {
break;
};
parked.lock().await.push(stream);
}
}
});
url
}
fn is_timeout_error<E: std::fmt::Display + std::fmt::Debug>(err: &E) -> bool {
let display = format!("{err}").to_lowercase();
let debug = format!("{err:?}").to_lowercase();
let needles = ["timed out", "timedout", "deadline"];
needles
.iter()
.any(|n| display.contains(n) || debug.contains(n))
}
#[tokio::test(flavor = "current_thread")]
async fn provider_config_timeout_is_applied_to_http_transport() {
let url = spawn_stalled_listener().await;
let provider =
create_http_provider(ProviderConfig::new(&url).with_timeout(Duration::from_millis(150)))
.expect("provider built");
let start = Instant::now();
let result = provider.get_block_number().await;
let elapsed = start.elapsed();
let err = result.expect_err("expected a transport error");
assert!(
is_timeout_error(&err),
"expected reqwest timeout error, got {err:?}"
);
assert!(
elapsed < Duration::from_secs(2),
"expected error within ~150ms, elapsed {elapsed:?}"
);
}
#[tokio::test(flavor = "current_thread")]
async fn provider_pool_applies_rpc_policy_timeout_per_chain() {
let url = spawn_stalled_listener().await;
let config = SemioscanConfigBuilder::with_defaults()
.chain_timeout(NamedChain::Mainnet, Duration::from_millis(150))
.build();
let pool = ProviderPoolBuilder::new()
.add_chain(NamedChain::Mainnet, &url)
.with_rpc_policy(&config)
.build()
.expect("pool built");
let provider = pool.get(NamedChain::Mainnet).expect("provider present");
let start = Instant::now();
let result = provider.get_block_number().await;
let elapsed = start.elapsed();
let err = result.expect_err("expected a transport error");
assert!(
is_timeout_error(&err),
"expected reqwest timeout error, got {err:?}"
);
assert!(
elapsed < Duration::from_secs(2),
"expected error within ~150ms, elapsed {elapsed:?}"
);
}
#[tokio::test(flavor = "current_thread")]
async fn provider_pool_endpoint_timeout_overrides_default() {
let url = spawn_stalled_listener().await;
let endpoint =
ChainEndpoint::new(NamedChain::Mainnet, &url).with_timeout(Duration::from_millis(150));
let pool = ProviderPoolBuilder::new()
.with_timeout(Duration::from_secs(30))
.add_endpoint(endpoint)
.build()
.expect("pool built");
let provider = pool.get(NamedChain::Mainnet).expect("provider present");
let start = Instant::now();
let result = provider.get_block_number().await;
let elapsed = start.elapsed();
let err = result.expect_err("expected a transport error");
assert!(
is_timeout_error(&err),
"expected reqwest timeout error, got {err:?}"
);
assert!(
elapsed < Duration::from_secs(2),
"endpoint timeout must override default; elapsed {elapsed:?}"
);
}
#[tokio::test(flavor = "current_thread")]
async fn endpoint_timeout_overrides_rpc_policy() {
let url = spawn_stalled_listener().await;
let config = SemioscanConfigBuilder::with_defaults()
.chain_timeout(NamedChain::Mainnet, Duration::from_secs(30))
.build();
let endpoint =
ChainEndpoint::new(NamedChain::Mainnet, &url).with_timeout(Duration::from_millis(150));
let pool = ProviderPoolBuilder::new()
.add_endpoint(endpoint)
.with_rpc_policy(&config)
.build()
.expect("pool built");
let provider = pool.get(NamedChain::Mainnet).expect("provider present");
let start = Instant::now();
let result = provider.get_block_number().await;
let elapsed = start.elapsed();
let err = result.expect_err("expected a transport error");
assert!(
is_timeout_error(&err),
"expected reqwest timeout error, got {err:?}"
);
assert!(
elapsed < Duration::from_secs(2),
"endpoint timeout must override policy timeout; elapsed {elapsed:?}"
);
}
#[test]
fn rpc_policy_resolution_is_exposed_via_public_api() {
let config = SemioscanConfigBuilder::with_defaults()
.rpc_timeout(Duration::from_secs(45))
.chain_timeout(NamedChain::Polygon, Duration::from_secs(90))
.build();
assert_eq!(
config.rpc_config(NamedChain::Mainnet).rpc_timeout,
Duration::from_secs(45)
);
assert_eq!(
config.rpc_config(NamedChain::Polygon).rpc_timeout,
Duration::from_secs(90)
);
}