Skip to content

Commit b2d133c

Browse files
committed
fix unreliable rpc tests due to race condition issue on get_available_port
1 parent 1f05b93 commit b2d133c

4 files changed

Lines changed: 40 additions & 15 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rpc/tests/cli.rs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::time::Duration;
44
use assert_cmd::assert::Assert;
55
use assert_cmd::cargo::cargo_bin_cmd;
66
use bdk_wallet::bitcoin::hex::test_hex_unwrap as hex;
7-
use bdk_wallet::bitcoin::{consensus, OutPoint, Transaction};
7+
use bdk_wallet::bitcoin::{OutPoint, Transaction, consensus};
88
use bdk_wallet::chain::{ChainPosition, ConfirmationBlockTime};
99
use bdk_wallet::{KeychainKind, LocalOutput};
1010
use const_format::str_replace;
@@ -13,10 +13,11 @@ use predicates::str;
1313
use rpc::server::{WalletImpl, WalletServer};
1414
use rpc::wallet::{TxConfidence, WalletService, WalletServiceImpl, WalletServiceMock, WalletTx};
1515
use testenv::TestEnv;
16+
use tokio::net::TcpListener;
1617
use tokio::task::{self, JoinHandle};
1718
use tonic::transport::server::TcpIncoming;
1819
use tonic::transport::{self, Server};
19-
use unimock::{matching, MockFn as _, Unimock};
20+
use unimock::{MockFn as _, Unimock, matching};
2021

2122
const CLI_TIMEOUT: Duration = Duration::from_millis(200);
2223

@@ -100,8 +101,12 @@ fn test_cli_no_connection() {
100101
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
101102
async fn test_cli_wallet_balance() {
102103
let testenv = TestEnv::new().expect("testEnv could not start");
103-
let port = testenv::get_available_port().expect("port");
104-
spawn_wallet_grpc_service(port, WalletServiceImpl::create_with_rpc_params(testenv.bitcoin_core_rpc_client().unwrap()));
104+
let listener = testenv::get_bound_port().await.expect("listener");
105+
let port = listener.local_addr().unwrap().port();
106+
spawn_wallet_grpc_service(
107+
listener,
108+
WalletServiceImpl::create_with_rpc_params(testenv.bitcoin_core_rpc_client().unwrap()),
109+
);
105110

106111
task::spawn_blocking(move || assert_cli_with_port(port, ["wallet-balance"]))
107112
.await.unwrap()
@@ -113,8 +118,12 @@ async fn test_cli_wallet_balance() {
113118
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
114119
async fn test_cli_new_address() {
115120
let testenv = TestEnv::new().expect("testEnv could not start"); // TODO: this doesnt make sense as a CLI make only sense if the bitcoind is
116-
let port = testenv::get_available_port().expect("port");
117-
spawn_wallet_grpc_service(port, WalletServiceImpl::create_with_rpc_params(testenv.bitcoin_core_rpc_client().unwrap()));
121+
let listener = testenv::get_bound_port().await.expect("listener");
122+
let port = listener.local_addr().unwrap().port();
123+
spawn_wallet_grpc_service(
124+
listener,
125+
WalletServiceImpl::create_with_rpc_params(testenv.bitcoin_core_rpc_client().unwrap()),
126+
);
118127

119128
task::spawn_blocking(move || assert_cli_with_port(port, ["new-address"]))
120129
.await.unwrap()
@@ -135,8 +144,9 @@ async fn test_cli_list_unspent() {
135144
.some_call(matching!()).returns(vec![mock_utxo()]);
136145
let mock_wallet_service = Unimock::new(clause).no_verify_in_drop();
137146

138-
let port = testenv::get_available_port().expect("port");
139-
spawn_wallet_grpc_service(port, mock_wallet_service);
147+
let listener = testenv::get_bound_port().await.expect("listener");
148+
let port = listener.local_addr().unwrap().port();
149+
spawn_wallet_grpc_service(listener, mock_wallet_service);
140150

141151
task::spawn_blocking(move || assert_cli_with_port(port, ["list-unspent"]))
142152
.await.unwrap()
@@ -153,8 +163,9 @@ async fn test_cli_notify_confidence() {
153163
.answers(&|_, _| mock_confidence_stream());
154164
let mock_wallet_service = Unimock::new(clause).no_verify_in_drop();
155165

156-
let port = testenv::get_available_port().expect("port");
157-
spawn_wallet_grpc_service(port, mock_wallet_service);
166+
let listener = testenv::get_bound_port().await.expect("listener");
167+
let port = listener.local_addr().unwrap().port();
168+
spawn_wallet_grpc_service(listener, mock_wallet_service);
158169

159170
task::spawn_blocking(move || assert_cli_with_port(port, ["notify-confidence",
160171
"37b560334094515cfdaa0146bfd4ce19e940064c505082031858b0aba3218990"]))
@@ -229,10 +240,12 @@ fn assert_cli_with_port<'a>(port: u16, args: impl IntoIterator<Item=&'a str>) ->
229240
assert_cli(args)
230241
}
231242

232-
fn spawn_wallet_grpc_service(port: u16, wallet_service: impl WalletService + Send + Sync + 'static)
233-
-> JoinHandle<Result<(), transport::Error>> {
243+
fn spawn_wallet_grpc_service(
244+
listener: TcpListener,
245+
wallet_service: impl WalletService + Send + Sync + 'static,
246+
) -> JoinHandle<Result<(), transport::Error>> {
234247
let wallet = WalletImpl { wallet_service: Arc::new(wallet_service) };
235-
let incoming = TcpIncoming::bind(format!("127.0.0.1:{port}").parse().unwrap()).unwrap();
248+
let incoming = TcpIncoming::from(listener);
236249

237250
task::spawn(async move {
238251
Server::builder()

testenv/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ secp = { workspace = true }
1515
simple-semaphore = { workspace = true }
1616
tempfile = { workspace = true }
1717
bmp_tracing = { workspace = true }
18+
tokio = { workspace = true }
1819

1920
electrsd = { version = "0.36.1", features = [
2021
"esplora_a33e97e1",

testenv/src/lib.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ use bdk_wallet::bitcoin::key::Secp256k1;
1414
use bdk_wallet::bitcoin::secp256k1::All;
1515
use bdk_wallet::bitcoin::{Address, Amount, BlockHash, Network, Transaction, Txid};
1616
use bmp_tracing::tracing;
17-
pub use corepc_node::get_available_port;
17+
use tokio::net::TcpListener;
18+
19+
/// Returns a TcpListener bound to an available port (port 0 lets OS assign).
20+
/// This avoids race conditions by keeping the port bound until used.
21+
pub async fn get_bound_port() -> Result<TcpListener> {
22+
TcpListener::bind("127.0.0.1:0").await.map_err(Into::into)
23+
}
1824
use electrsd::corepc_node::Node;
1925
use electrsd::electrum_client::{Client, ElectrumApi};
2026
use electrsd::{ElectrsD, corepc_node};
@@ -235,7 +241,8 @@ impl TestEnv {
235241
pub fn start_explorer_in_container(&mut self) -> Result<()> {
236242
// this start a container for debugging
237243
let bitcoind_rpc_port = self.bitcoin_rpc_port();
238-
let browser_port = get_available_port()?;
244+
let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
245+
let browser_port = listener.local_addr()?.port();
239246

240247
let electrum_port = self.electrsd.electrum_url.split(':').next_back()
241248
.context("Failed to parse electrum port")?;
@@ -267,6 +274,9 @@ impl TestEnv {
267274
self.explorer_process = Some(child);
268275
self.container_name = Some(container_name);
269276

277+
// Drop the listener to free the port for the container
278+
drop(listener);
279+
270280
tracing::info!(
271281
"Starting explorer in container, access it at http://127.0.0.1:{browser_port}/blocks"
272282
);

0 commit comments

Comments
 (0)