Skip to content

Commit f25c0bd

Browse files
authored
Merge pull request #132 from sdmg15/fix/rpc-tests
fix unreliable rpc tests due to race condition issue on get_available_port
2 parents 4885480 + 5650a7e commit f25c0bd

File tree

4 files changed

+38
-15
lines changed

4 files changed

+38
-15
lines changed

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: 22 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,11 @@ 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 (port, listener) = TestEnv::get_bound_port().await.expect("listener");
105+
spawn_wallet_grpc_service(
106+
listener,
107+
WalletServiceImpl::create_with_rpc_params(testenv.bitcoin_core_rpc_client().unwrap()),
108+
);
105109

106110
task::spawn_blocking(move || assert_cli_with_port(port, ["wallet-balance"]))
107111
.await.unwrap()
@@ -113,8 +117,11 @@ async fn test_cli_wallet_balance() {
113117
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
114118
async fn test_cli_new_address() {
115119
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()));
120+
let (port, listener) = TestEnv::get_bound_port().await.expect("listener");
121+
spawn_wallet_grpc_service(
122+
listener,
123+
WalletServiceImpl::create_with_rpc_params(testenv.bitcoin_core_rpc_client().unwrap()),
124+
);
118125

119126
task::spawn_blocking(move || assert_cli_with_port(port, ["new-address"]))
120127
.await.unwrap()
@@ -135,8 +142,8 @@ async fn test_cli_list_unspent() {
135142
.some_call(matching!()).returns(vec![mock_utxo()]);
136143
let mock_wallet_service = Unimock::new(clause).no_verify_in_drop();
137144

138-
let port = testenv::get_available_port().expect("port");
139-
spawn_wallet_grpc_service(port, mock_wallet_service);
145+
let (port, listener) = TestEnv::get_bound_port().await.expect("listener");
146+
spawn_wallet_grpc_service(listener, mock_wallet_service);
140147

141148
task::spawn_blocking(move || assert_cli_with_port(port, ["list-unspent"]))
142149
.await.unwrap()
@@ -153,8 +160,8 @@ async fn test_cli_notify_confidence() {
153160
.answers(&|_, _| mock_confidence_stream());
154161
let mock_wallet_service = Unimock::new(clause).no_verify_in_drop();
155162

156-
let port = testenv::get_available_port().expect("port");
157-
spawn_wallet_grpc_service(port, mock_wallet_service);
163+
let (port, listener) = TestEnv::get_bound_port().await.expect("listener");
164+
spawn_wallet_grpc_service(listener, mock_wallet_service);
158165

159166
task::spawn_blocking(move || assert_cli_with_port(port, ["notify-confidence",
160167
"37b560334094515cfdaa0146bfd4ce19e940064c505082031858b0aba3218990"]))
@@ -229,10 +236,12 @@ fn assert_cli_with_port<'a>(port: u16, args: impl IntoIterator<Item=&'a str>) ->
229236
assert_cli(args)
230237
}
231238

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

237246
task::spawn(async move {
238247
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: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ 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;
1818
use electrsd::corepc_node::Node;
1919
use electrsd::electrum_client::{Client, ElectrumApi};
2020
use electrsd::{ElectrsD, corepc_node};
@@ -235,7 +235,8 @@ impl TestEnv {
235235
pub fn start_explorer_in_container(&mut self) -> Result<()> {
236236
// this start a container for debugging
237237
let bitcoind_rpc_port = self.bitcoin_rpc_port();
238-
let browser_port = get_available_port()?;
238+
let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
239+
let browser_port = listener.local_addr()?.port();
239240

240241
let electrum_port = self.electrsd.electrum_url.split(':').next_back()
241242
.context("Failed to parse electrum port")?;
@@ -267,6 +268,9 @@ impl TestEnv {
267268
self.explorer_process = Some(child);
268269
self.container_name = Some(container_name);
269270

271+
// Drop the listener to free the port for the container
272+
drop(listener);
273+
270274
tracing::info!(
271275
"Starting explorer in container, access it at http://127.0.0.1:{browser_port}/blocks"
272276
);
@@ -458,6 +462,14 @@ impl TestEnv {
458462
pub fn p2p_socket_addr(&self) -> Option<SocketAddrV4> {
459463
self.bitcoind.params.p2p_socket
460464
}
465+
466+
/// Returns a TcpListener bound to an available port (port 0 lets OS assign).
467+
/// This avoids race conditions by keeping the port bound until used.
468+
pub async fn get_bound_port() -> Result<(u16, TcpListener)> {
469+
let listener = TcpListener::bind("127.0.0.1:0").await?;
470+
let port = listener.local_addr().unwrap().port();
471+
Ok((port, listener))
472+
}
461473
}
462474

463475
impl Drop for TestEnv {

0 commit comments

Comments
 (0)