From 0d3fd1f70b44f8a3e09a6404e96e40d9f467c740 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Tue, 12 May 2026 14:26:48 -0500 Subject: [PATCH] Prevent Rust test shutdown hangs The _test-utils integration suite can stall during teardown when the LDK node in the dummy trusted wallet does not finish stopping. This keeps CI failures tied to the offending test instead of waiting for the whole job timeout. --- orange-sdk/src/trusted_wallet/dummy.rs | 11 +++++- orange-sdk/tests/integration_tests.rs | 5 +-- orange-sdk/tests/test_utils.rs | 54 +++++++++++++++++++++----- 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/orange-sdk/src/trusted_wallet/dummy.rs b/orange-sdk/src/trusted_wallet/dummy.rs index e19e8da..e223e01 100644 --- a/orange-sdk/src/trusted_wallet/dummy.rs +++ b/orange-sdk/src/trusted_wallet/dummy.rs @@ -431,7 +431,16 @@ impl TrustedWalletInterface for DummyTrustedWallet { fn stop(&self) -> Pin + Send + '_>> { Box::pin(async move { - let _ = self.ldk_node.stop(); + let ldk_node = Arc::clone(&self.ldk_node); + let (sender, receiver) = tokio::sync::oneshot::channel(); + std::thread::spawn(move || { + let _ = ldk_node.stop(); + let _ = sender.send(()); + }); + + if tokio::time::timeout(Duration::from_secs(10), receiver).await.is_err() { + eprintln!("Warning: dummy trusted LDK node stop timed out"); + } }) } } diff --git a/orange-sdk/tests/integration_tests.rs b/orange-sdk/tests/integration_tests.rs index 5a654ae..2ca01e6 100644 --- a/orange-sdk/tests/integration_tests.rs +++ b/orange-sdk/tests/integration_tests.rs @@ -9,7 +9,6 @@ use ldk_node::bitcoin::Network; use ldk_node::lightning_invoice::{Bolt11InvoiceDescription, Description}; use ldk_node::payment::{ConfirmationStatus, PaymentDirection, PaymentStatus}; use log::info; -use orange_sdk::bitcoin::hashes::Hash; use orange_sdk::{Event, PaymentInfo, PaymentType, TxStatus, WalletError}; use std::sync::Arc; use std::time::Duration; @@ -597,7 +596,7 @@ async fn test_receive_to_onchain_with_channel() { } async fn run_test_pay_lightning_from_self_custody(amountless: bool) { - test_utils::run_test(|params| async move { + test_utils::run_test(move |params| async move { let wallet = Arc::clone(¶ms.wallet); let bitcoind = Arc::clone(¶ms.bitcoind); let third_party = Arc::clone(¶ms.third_party); @@ -701,7 +700,7 @@ async fn test_pay_lightning_from_self_custody() { } async fn run_test_pay_bolt12_from_self_custody(amountless: bool) { - test_utils::run_test(|params| async move { + test_utils::run_test(move |params| async move { let wallet = Arc::clone(¶ms.wallet); let bitcoind = Arc::clone(¶ms.bitcoind); let third_party = Arc::clone(¶ms.third_party); diff --git a/orange-sdk/tests/test_utils.rs b/orange-sdk/tests/test_utils.rs index fb7de82..c9defd5 100644 --- a/orange-sdk/tests/test_utils.rs +++ b/orange-sdk/tests/test_utils.rs @@ -11,7 +11,6 @@ use corepc_node::client::bitcoin::Network; use corepc_node::{Conf, Node as Bitcoind, get_available_port}; use electrsd::ElectrsD; use electrsd::electrum_client::ElectrumApi; -use ldk_node::bitcoin::hashes::Hash; use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::PaymentStatus; @@ -273,8 +272,22 @@ impl TestParams { #[cfg(feature = "_cashu-tests")] let _ = self._mint.stop().await; - let _ = self.lsp.stop(); - let _ = self.third_party.stop(); + tokio::join!( + stop_ldk_node("lsp", Arc::clone(&self.lsp)), + stop_ldk_node("third_party", Arc::clone(&self.third_party)), + ); + } +} + +async fn stop_ldk_node(name: &'static str, node: Arc) { + let (sender, receiver) = tokio::sync::oneshot::channel(); + std::thread::spawn(move || { + let _ = node.stop(); + let _ = sender.send(()); + }); + + if tokio::time::timeout(Duration::from_secs(10), receiver).await.is_err() { + eprintln!("Warning: {name} LDK node stop timed out"); } } @@ -283,20 +296,43 @@ impl TestParams { /// Cleanup happens automatically after the test completes. pub async fn run_test(test: F) where - F: FnOnce(TestParams) -> Fut, - Fut: Future, + F: FnOnce(TestParams) -> Fut + Send + 'static, + Fut: Future + Send + 'static, { let params = build_test_nodes().await; println!("=== test start ==="); - // Run the test and get params back - test(params.clone()).await; + let test_timeout = if std::env::var("CI").is_ok() { + Duration::from_secs(15 * 60) + } else { + Duration::from_secs(5 * 60) + }; + + let mut test_task = tokio::spawn({ + let params = params.clone(); + async move { test(params).await } + }); + let test_result = tokio::select! { + res = &mut test_task => Ok(res), + _ = tokio::time::sleep(test_timeout) => { + test_task.abort(); + let _ = test_task.await; + Err(()) + }, + }; // Always clean up - let timeout = Duration::from_secs(10); + let timeout = Duration::from_secs(30); if tokio::time::timeout(timeout, params.stop()).await.is_err() { - eprintln!("Warning: parms stop timed out after {timeout:?}"); + eprintln!("Warning: params stop timed out after {timeout:?}"); + } + + match test_result { + Ok(Ok(())) => {}, + Ok(Err(e)) if e.is_panic() => std::panic::resume_unwind(e.into_panic()), + Ok(Err(e)) => panic!("test task failed: {e}"), + Err(_) => panic!("test timed out after {test_timeout:?}"), } }