Skip to content

Commit 9f100e1

Browse files
committed
refactor(test): DRY up interop test harness
Address review feedback on verbosity and duplication: - Extract common tests (10 scenarios) into `interop_basic_tests!` macro, reducing CLN/LND/Eclair test files from 743 to 343 lines (-54%) - Move `make_error()` from 3 implementations to ExternalNode trait default - Extract reconnection polling into `reconnect_and_wait()` helper - Add Windows exclusion for bitcoind/electrs env vars (binaries not downloaded) - Add newline in channel.rs per review nit - Update Eclair ignore messages with issue links and clearer wording
1 parent fa3597a commit 9f100e1

11 files changed

Lines changed: 211 additions & 459 deletions

File tree

.github/workflows/rust.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ jobs:
6868
mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }}
6969
mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }}
7070
- name: Set bitcoind/electrs environment variables
71+
if: "matrix.platform != 'windows-latest'"
7172
run: |
7273
echo "BITCOIND_EXE=$( pwd )/bin/bitcoind-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV"
7374
echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV"

tests/common/cln.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,6 @@ impl TestClnNode {
5252
tokio::task::spawn_blocking(move || f(&*client)).await.expect("CLN RPC task panicked")
5353
}
5454

55-
fn make_error(&self, detail: String) -> TestFailure {
56-
TestFailure::ExternalNodeError { node: "CLN".to_string(), detail }
57-
}
58-
5955
/// Repeatedly call `splice_update` until `commitments_secured` is true.
6056
/// Returns the final PSBT. Gives up after 10 attempts.
6157
async fn splice_update_loop(

tests/common/eclair.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,6 @@ impl TestEclairNode {
7878
})
7979
}
8080

81-
fn make_error(&self, detail: String) -> TestFailure {
82-
TestFailure::ExternalNodeError { node: "Eclair".to_string(), detail }
83-
}
84-
8581
/// Poll /getsentinfo until the payment settles or fails.
8682
/// Eclair's pay/keysend APIs are fire-and-forget, so without polling
8783
/// the caller would hang on the LDK event timeout when a payment fails.

tests/common/external_node.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ pub(crate) trait ExternalNode: Send + Sync {
106106
/// List all channels known to this node.
107107
async fn list_channels(&self) -> Result<Vec<ExternalChannel>, TestFailure>;
108108

109+
/// Construct a `TestFailure::ExternalNodeError` for this node.
110+
fn make_error(&self, detail: String) -> TestFailure {
111+
TestFailure::ExternalNodeError { node: self.name().to_string(), detail }
112+
}
113+
109114
/// Wait until this node has synced to at least `min_height`.
110115
///
111116
/// The default is a no-op — most implementations (LND, CLN) sync blocks

tests/common/lnd.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,6 @@ impl TestLndNode {
5555
.unwrap();
5656
Self::new(cert_path, macaroon_path, endpoint, listen_addr).await
5757
}
58-
59-
fn make_error(&self, detail: String) -> TestFailure {
60-
TestFailure::ExternalNodeError { node: "LND".to_string(), detail }
61-
}
6258
}
6359

6460
#[async_trait]

tests/common/mod.rs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1793,6 +1793,163 @@ impl TestSyncStoreInner {
17931793
/// ```ignore
17941794
/// interop_combo_tests!(test_lnd, setup_clients, setup_ldk_node);
17951795
/// ```
1796+
///
1797+
/// ## `interop_basic_tests!`
1798+
///
1799+
/// Generates the standard named tests that are identical across all external
1800+
/// node implementations: basic channel cycle, disconnect/reconnect, force-close
1801+
/// (by LDK and external), inbound channel, bidirectional payments, expired
1802+
/// invoice, concurrent payments, and fee-change close scenarios.
1803+
///
1804+
/// Tests that differ per-implementation (e.g. keysend, splicing) should remain
1805+
/// as hand-written functions in the individual test files.
1806+
///
1807+
/// Usage:
1808+
/// ```ignore
1809+
/// interop_basic_tests!(test_lnd, setup_clients, setup_ldk_node);
1810+
/// ```
1811+
#[macro_export]
1812+
macro_rules! interop_basic_tests {
1813+
($prefix:ident, $setup_clients:ident, $setup_ldk_node:ident) => {
1814+
paste::paste! {
1815+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1816+
async fn [<$prefix _basic_channel_cycle>]() {
1817+
let (bitcoind, electrs, peer) = $setup_clients().await;
1818+
let node = $setup_ldk_node();
1819+
setup_interop_test(&node, &peer, &bitcoind, &electrs).await;
1820+
1821+
let (user_channel_id, _ext_channel_id) =
1822+
open_channel_to_external(
1823+
&node, &peer, &bitcoind, &electrs, 1_000_000, Some(500_000_000),
1824+
).await;
1825+
1826+
let invoice = peer.create_invoice(10_000_000, "basic-send").await.unwrap();
1827+
let parsed = lightning_invoice::Bolt11Invoice::from_str(&invoice).unwrap();
1828+
node.bolt11_payment().send(&parsed, None).unwrap();
1829+
$crate::common::expect_event!(node, PaymentSuccessful);
1830+
1831+
receive_bolt11_payment(&node, &peer, 10_000_000).await;
1832+
1833+
cooperative_close_by_ldk(
1834+
&node, &peer, &bitcoind, &electrs, &user_channel_id,
1835+
).await;
1836+
node.stop().unwrap();
1837+
}
1838+
1839+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1840+
async fn [<$prefix _disconnect_reconnect>]() {
1841+
let (bitcoind, electrs, peer) = $setup_clients().await;
1842+
let node = $setup_ldk_node();
1843+
setup_interop_test(&node, &peer, &bitcoind, &electrs).await;
1844+
let (_user_ch, _ext_ch) = open_channel_to_external(
1845+
&node, &peer, &bitcoind, &electrs, 1_000_000, Some(500_000_000),
1846+
).await;
1847+
1848+
disconnect_reconnect_idle(
1849+
&node, &peer, &bitcoind, &electrs, &Side::Ldk,
1850+
).await;
1851+
disconnect_reconnect_idle(
1852+
&node, &peer, &bitcoind, &electrs, &Side::External,
1853+
).await;
1854+
1855+
node.stop().unwrap();
1856+
}
1857+
1858+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1859+
async fn [<$prefix _force_close_by_ldk>]() {
1860+
let (bitcoind, electrs, peer) = $setup_clients().await;
1861+
let node = $setup_ldk_node();
1862+
setup_interop_test(&node, &peer, &bitcoind, &electrs).await;
1863+
let (user_ch, _ext_ch) = open_channel_to_external(
1864+
&node, &peer, &bitcoind, &electrs, 1_000_000, Some(500_000_000),
1865+
).await;
1866+
1867+
force_close_by_ldk(&node, &peer, &bitcoind, &electrs, &user_ch).await;
1868+
node.stop().unwrap();
1869+
}
1870+
1871+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1872+
async fn [<$prefix _force_close_by_external>]() {
1873+
let (bitcoind, electrs, peer) = $setup_clients().await;
1874+
let node = $setup_ldk_node();
1875+
setup_interop_test(&node, &peer, &bitcoind, &electrs).await;
1876+
let (_user_ch, ext_ch) = open_channel_to_external(
1877+
&node, &peer, &bitcoind, &electrs, 1_000_000, Some(500_000_000),
1878+
).await;
1879+
1880+
force_close_by_external(&node, &peer, &bitcoind, &electrs, &ext_ch).await;
1881+
node.stop().unwrap();
1882+
}
1883+
1884+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1885+
async fn [<$prefix _inbound_channel>]() {
1886+
let (bitcoind, electrs, peer) = $setup_clients().await;
1887+
let node = $setup_ldk_node();
1888+
run_inbound_channel_test(
1889+
&node, &peer, &bitcoind, &electrs,
1890+
CloseType::Cooperative, Side::External,
1891+
).await;
1892+
node.stop().unwrap();
1893+
}
1894+
1895+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1896+
async fn [<$prefix _bidirectional_payments>]() {
1897+
let (bitcoind, electrs, peer) = $setup_clients().await;
1898+
let node = $setup_ldk_node();
1899+
setup_interop_test(&node, &peer, &bitcoind, &electrs).await;
1900+
let (_user_ch, ext_ch) = open_channel_to_external(
1901+
&node, &peer, &bitcoind, &electrs, 1_000_000, Some(500_000_000),
1902+
).await;
1903+
1904+
bidirectional_payments(&node, &peer, &ext_ch, 5_000_000).await;
1905+
node.stop().unwrap();
1906+
}
1907+
1908+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1909+
async fn [<$prefix _pay_expired_invoice>]() {
1910+
let (bitcoind, electrs, peer) = $setup_clients().await;
1911+
let node = $setup_ldk_node();
1912+
setup_interop_test(&node, &peer, &bitcoind, &electrs).await;
1913+
let (_user_ch, _ext_ch) = open_channel_to_external(
1914+
&node, &peer, &bitcoind, &electrs, 1_000_000, Some(500_000_000),
1915+
).await;
1916+
1917+
pay_expired_invoice(&node, &peer).await;
1918+
node.stop().unwrap();
1919+
}
1920+
1921+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1922+
async fn [<$prefix _concurrent_payments>]() {
1923+
let (bitcoind, electrs, peer) = $setup_clients().await;
1924+
let node = $setup_ldk_node();
1925+
setup_interop_test(&node, &peer, &bitcoind, &electrs).await;
1926+
let (_user_ch, _ext_ch) = open_channel_to_external(
1927+
&node, &peer, &bitcoind, &electrs, 1_000_000, Some(500_000_000),
1928+
).await;
1929+
1930+
concurrent_payments(&node, &peer, 5, 1_000_000).await;
1931+
node.stop().unwrap();
1932+
}
1933+
1934+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1935+
async fn [<$prefix _cooperative_close_after_fee_change>]() {
1936+
let (bitcoind, electrs, peer) = $setup_clients().await;
1937+
let node = $setup_ldk_node();
1938+
cooperative_close_after_fee_change(&node, &peer, &bitcoind, &electrs).await;
1939+
node.stop().unwrap();
1940+
}
1941+
1942+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1943+
async fn [<$prefix _force_close_after_fee_change>]() {
1944+
let (bitcoind, electrs, peer) = $setup_clients().await;
1945+
let node = $setup_ldk_node();
1946+
force_close_after_fee_change(&node, &peer, &bitcoind, &electrs).await;
1947+
node.stop().unwrap();
1948+
}
1949+
}
1950+
};
1951+
}
1952+
17961953
#[macro_export]
17971954
macro_rules! interop_combo_tests {
17981955
($prefix:ident, $setup_clients:ident, $setup_ldk_node:ident) => {

tests/common/scenarios/channel.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ async fn expect_channel_closed(node: &Node, expected_local_initiated: bool) {
2727
)
2828
.await
2929
.unwrap_or_else(|_| panic!("{} timed out waiting for ChannelClosed event", node.node_id()));
30+
3031
match event {
3132
Event::ChannelClosed { ref reason, .. } => {
3233
println!("{} got ChannelClosed: reason={:?}", node.node_id(), reason);

tests/common/scenarios/disconnect.rs

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,30 @@ use std::time::Duration;
1010

1111
use electrsd::corepc_node::Client as BitcoindClient;
1212
use electrum_client::ElectrumApi;
13+
use ldk_node::bitcoin::secp256k1::PublicKey;
14+
use ldk_node::lightning::ln::msgs::SocketAddress;
1315
use ldk_node::{Event, Node};
1416
use lightning_invoice::Bolt11Invoice;
1517

1618
use super::super::external_node::ExternalNode;
1719
use super::Side;
1820

21+
/// Reconnect to a peer and wait until the connection is established.
22+
async fn reconnect_and_wait(node: &Node, peer_id: PublicKey, addr: SocketAddress, context: &str) {
23+
node.connect(peer_id, addr, true).unwrap();
24+
for i in 0..30 {
25+
if node.list_peers().iter().any(|p| p.node_id == peer_id && p.is_connected) {
26+
// Allow channel reestablishment to complete
27+
tokio::time::sleep(Duration::from_secs(2)).await;
28+
return;
29+
}
30+
if i == 29 {
31+
panic!("Peer did not reconnect within 30s ({})", context);
32+
}
33+
tokio::time::sleep(Duration::from_secs(1)).await;
34+
}
35+
}
36+
1937
/// Disconnect during idle, reconnect, verify channel still works.
2038
pub(crate) async fn disconnect_reconnect_idle<E: ElectrumApi>(
2139
node: &Node, peer: &(impl ExternalNode + ?Sized), _bitcoind: &BitcoindClient, _electrs: &E,
@@ -35,21 +53,7 @@ pub(crate) async fn disconnect_reconnect_idle<E: ElectrumApi>(
3553

3654
tokio::time::sleep(Duration::from_secs(1)).await;
3755

38-
// Reconnect and wait for channel reestablishment
39-
node.connect(ext_node_id, ext_addr, true).unwrap();
40-
for i in 0..30 {
41-
let connected =
42-
node.list_peers().iter().any(|p| p.node_id == ext_node_id && p.is_connected);
43-
if connected {
44-
break;
45-
}
46-
if i == 29 {
47-
panic!("Peer did not reconnect within 30s after idle disconnect");
48-
}
49-
tokio::time::sleep(Duration::from_secs(1)).await;
50-
}
51-
// Allow channel reestablishment to complete
52-
tokio::time::sleep(Duration::from_secs(2)).await;
56+
reconnect_and_wait(node, ext_node_id, ext_addr, "idle disconnect").await;
5357

5458
// Verify channel still works with a payment
5559
let invoice_str = peer.create_invoice(10_000_000, "disconnect-idle-test").await.unwrap();
@@ -86,8 +90,7 @@ pub(crate) async fn disconnect_during_payment<E: ElectrumApi>(
8690
tokio::time::sleep(Duration::from_secs(2)).await;
8791

8892
// Reconnect
89-
node.connect(ext_node_id, ext_addr, true).unwrap();
90-
tokio::time::sleep(Duration::from_secs(2)).await;
93+
reconnect_and_wait(node, ext_node_id, ext_addr, "disconnect during payment").await;
9194

9295
// If the payment was initiated, wait for it to resolve.
9396
if send_ok {

0 commit comments

Comments
 (0)