Skip to content

Commit d86df20

Browse files
committed
Add e2e config startup tests
Resolve the e2e harness conflicts around gRPC config startup and add coverage for supported configuration variants and startup failures. AI-assisted-by: OpenAI Codex
1 parent 2b41501 commit d86df20

3 files changed

Lines changed: 404 additions & 42 deletions

File tree

e2e-tests/src/lib.rs

Lines changed: 259 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ use std::time::Duration;
1616
use corepc_node::Node;
1717
use hex_conservative::DisplayHex;
1818
use ldk_server_client::client::LdkServerClient;
19-
use serde_json::Value;
2019
use ldk_server_client::ldk_server_grpc::api::{GetNodeInfoRequest, GetNodeInfoResponse};
2120
use ldk_server_grpc::api::{
2221
GetBalancesRequest, ListChannelsRequest, OnchainReceiveRequest, OpenChannelRequest,
2322
};
23+
use serde_json::Value;
2424

2525
/// Wrapper around a managed bitcoind process for regtest.
2626
pub struct TestBitcoind {
@@ -103,42 +103,159 @@ pub struct LdkServerConfig {
103103
pub metrics_auth: Option<(String, String)>,
104104
}
105105

106-
impl LdkServerHandle {
107-
/// Starts a new ldk-server instance against the given bitcoind.
108-
/// Waits until the server is ready to accept requests.
109-
pub async fn start(bitcoind: &TestBitcoind) -> Self {
110-
Self::start_with_config(bitcoind, LdkServerConfig::default()).await
106+
/// Dynamic parameters available when building test configs.
107+
pub struct TestServerParams {
108+
pub grpc_port: u16,
109+
pub p2p_port: u16,
110+
pub storage_dir: PathBuf,
111+
pub rpc_address: String,
112+
pub rpc_user: String,
113+
pub rpc_password: String,
114+
}
115+
116+
/// A chain source for the test config, mirroring the server's supported backends.
117+
pub enum ChainSource {
118+
Bitcoind { rpc_address: String, rpc_user: String, rpc_password: String },
119+
Electrum { server_url: String },
120+
Esplora { server_url: String },
121+
}
122+
123+
impl ChainSource {
124+
/// Render the chain source as its TOML section.
125+
fn to_toml(&self) -> String {
126+
match self {
127+
ChainSource::Bitcoind { rpc_address, rpc_user, rpc_password } => format!(
128+
"[bitcoind]\nrpc_address = \"{}\"\nrpc_user = \"{}\"\nrpc_password = \"{}\"",
129+
rpc_address, rpc_user, rpc_password
130+
),
131+
ChainSource::Electrum { server_url } => {
132+
format!("[electrum]\nserver_url = \"{}\"", server_url)
133+
},
134+
ChainSource::Esplora { server_url } => {
135+
format!("[esplora]\nserver_url = \"{}\"", server_url)
136+
},
137+
}
138+
}
139+
}
140+
141+
/// Builder for the ldk-server config TOML used in tests.
142+
///
143+
/// Tests tweak named, typed knobs and call [`TestConfigBuilder::build`] once to
144+
/// produce the TOML. This keeps tests from doing string surgery on rendered output.
145+
pub struct TestConfigBuilder {
146+
listening_addresses: Vec<String>,
147+
announcement_addresses: Vec<String>,
148+
grpc_service_address: String,
149+
alias: Option<String>,
150+
storage_dir: PathBuf,
151+
chain_source: ChainSource,
152+
metrics_auth: Option<(String, String)>,
153+
log: Option<(Option<String>, String)>,
154+
tls_hosts: Option<Vec<String>>,
155+
}
156+
157+
impl TestConfigBuilder {
158+
/// Start from the default test config: a single localhost listening address, the
159+
/// `e2e-test-node` alias, and a bitcoind RPC chain source derived from `params`.
160+
pub fn new(params: &TestServerParams) -> Self {
161+
Self {
162+
listening_addresses: vec![format!("127.0.0.1:{}", params.p2p_port)],
163+
announcement_addresses: Vec::new(),
164+
grpc_service_address: format!("127.0.0.1:{}", params.grpc_port),
165+
alias: Some("e2e-test-node".to_string()),
166+
storage_dir: params.storage_dir.clone(),
167+
chain_source: ChainSource::Bitcoind {
168+
rpc_address: params.rpc_address.clone(),
169+
rpc_user: params.rpc_user.clone(),
170+
rpc_password: params.rpc_password.clone(),
171+
},
172+
metrics_auth: None,
173+
log: None,
174+
tls_hosts: None,
175+
}
176+
}
177+
178+
/// Set the node alias, or `None` to omit it entirely.
179+
pub fn alias(mut self, alias: Option<&str>) -> Self {
180+
self.alias = alias.map(str::to_string);
181+
self
182+
}
183+
184+
/// Set the listening addresses. An empty vec omits the key entirely.
185+
pub fn listening_addresses(mut self, addresses: Vec<String>) -> Self {
186+
self.listening_addresses = addresses;
187+
self
188+
}
189+
190+
/// Set the announcement addresses. An empty vec (the default) omits the key.
191+
pub fn announcement_addresses(mut self, addresses: Vec<String>) -> Self {
192+
self.announcement_addresses = addresses;
193+
self
194+
}
195+
196+
/// Replace the chain source backend.
197+
pub fn chain_source(mut self, chain_source: ChainSource) -> Self {
198+
self.chain_source = chain_source;
199+
self
200+
}
201+
202+
/// Add HTTP basic auth credentials to the `[metrics]` section.
203+
pub fn metrics_auth(mut self, username: &str, password: &str) -> Self {
204+
self.metrics_auth = Some((username.to_string(), password.to_string()));
205+
self
206+
}
207+
208+
/// Add a `[log]` section with the given file path and optional level.
209+
pub fn log(mut self, level: Option<&str>, file: &str) -> Self {
210+
self.log = Some((level.map(str::to_string), file.to_string()));
211+
self
111212
}
112213

113-
pub async fn start_with_config(bitcoind: &TestBitcoind, config: LdkServerConfig) -> Self {
114-
#[allow(deprecated)]
115-
let storage_dir = tempfile::tempdir().unwrap().into_path();
116-
let grpc_port = find_available_port();
117-
let p2p_port = find_available_port();
214+
/// Add a `[tls]` section advertising the given hosts.
215+
pub fn tls_hosts(mut self, hosts: Vec<String>) -> Self {
216+
self.tls_hosts = Some(hosts);
217+
self
218+
}
219+
220+
/// Build the config into a TOML string.
221+
pub fn build(&self) -> String {
222+
fn toml_string_array(values: &[String]) -> String {
223+
let quoted: Vec<String> = values.iter().map(|v| format!("\"{}\"", v)).collect();
224+
format!("[{}]", quoted.join(", "))
225+
}
118226

119-
let (rpc_host, rpc_port_num, rpc_user, rpc_password) = bitcoind.rpc_details();
120-
let rpc_address = format!("{rpc_host}:{rpc_port_num}");
227+
let mut node = vec!["[node]".to_string(), "network = \"regtest\"".to_string()];
228+
if !self.listening_addresses.is_empty() {
229+
node.push(format!(
230+
"listening_addresses = {}",
231+
toml_string_array(&self.listening_addresses)
232+
));
233+
}
234+
node.push(format!("grpc_service_address = \"{}\"", self.grpc_service_address));
235+
if let Some(alias) = &self.alias {
236+
node.push(format!("alias = \"{}\"", alias));
237+
}
238+
if !self.announcement_addresses.is_empty() {
239+
node.push(format!(
240+
"announcement_addresses = {}",
241+
toml_string_array(&self.announcement_addresses)
242+
));
243+
}
121244

122-
let metrics_auth_config = if let Some((user, pass)) = config.metrics_auth {
123-
format!("username = \"{}\"\npassword = \"{}\"", user, pass)
124-
} else {
125-
String::new()
245+
let metrics_auth = match &self.metrics_auth {
246+
Some((user, pass)) => {
247+
format!("\nusername = \"{}\"\npassword = \"{}\"", user, pass)
248+
},
249+
None => String::new(),
126250
};
127251

128-
let config_content = format!(
129-
r#"[node]
130-
network = "regtest"
131-
listening_addresses = ["127.0.0.1:{p2p_port}"]
132-
grpc_service_address = "127.0.0.1:{grpc_port}"
133-
alias = "e2e-test-node"
252+
let mut config = format!(
253+
r#"{node}
134254
135255
[storage.disk]
136256
dir_path = "{storage_dir}"
137257
138-
[bitcoind]
139-
rpc_address = "{rpc_address}"
140-
rpc_user = "{rpc_user}"
141-
rpc_password = "{rpc_password}"
258+
{chain_source}
142259
143260
[liquidity.lsps2_service]
144261
advertise_service = false
@@ -154,24 +271,53 @@ disable_client_reserve = false
154271
155272
[metrics]
156273
enabled = true
157-
poll_metrics_interval = 1
158-
{metrics_auth_config}
274+
poll_metrics_interval = 1{metrics_auth}
159275
"#,
160-
storage_dir = storage_dir.display(),
276+
node = node.join("\n"),
277+
storage_dir = self.storage_dir.display(),
278+
chain_source = self.chain_source.to_toml(),
279+
metrics_auth = metrics_auth,
161280
);
162281

163-
let config_path = storage_dir.join("config.toml");
164-
std::fs::write(&config_path, &config_content).unwrap();
282+
if let Some((level, file)) = &self.log {
283+
config.push_str("\n[log]\n");
284+
if let Some(level) = level {
285+
config.push_str(&format!("level = \"{}\"\n", level));
286+
}
287+
config.push_str(&format!("file = \"{}\"\n", file));
288+
}
165289

166-
let server_binary = server_binary_path();
167-
let mut child = Command::new(&server_binary)
168-
.arg(config_path.to_str().unwrap())
169-
.stdout(Stdio::piped())
170-
.stderr(Stdio::piped())
171-
.spawn()
172-
.unwrap_or_else(|e| {
173-
panic!("Failed to start ldk-server binary at {:?}: {}", server_binary, e)
174-
});
290+
if let Some(hosts) = &self.tls_hosts {
291+
config.push_str(&format!("\n[tls]\nhosts = {}\n", toml_string_array(hosts)));
292+
}
293+
294+
config
295+
}
296+
}
297+
298+
impl LdkServerHandle {
299+
/// Starts a new ldk-server instance against the given bitcoind.
300+
/// Waits until the server is ready to accept requests.
301+
pub async fn start(bitcoind: &TestBitcoind) -> Self {
302+
Self::start_with_options(bitcoind, LdkServerConfig::default()).await
303+
}
304+
305+
pub async fn start_with_options(bitcoind: &TestBitcoind, config: LdkServerConfig) -> Self {
306+
Self::start_with_config(bitcoind, |params| {
307+
let mut builder = TestConfigBuilder::new(params);
308+
if let Some((user, pass)) = &config.metrics_auth {
309+
builder = builder.metrics_auth(user, pass);
310+
}
311+
builder.build()
312+
})
313+
.await
314+
}
315+
316+
pub async fn start_with_config(
317+
config_bitcoind: &TestBitcoind, config: impl FnOnce(&TestServerParams) -> String,
318+
) -> Self {
319+
let (mut child, params, config_path) = spawn_server(config_bitcoind, config);
320+
let TestServerParams { grpc_port, p2p_port, storage_dir, .. } = params;
175321

176322
// Spawn threads to forward stdout and stderr for debugging
177323
let stdout = child.stdout.take().unwrap();
@@ -251,6 +397,78 @@ impl Drop for LdkServerHandle {
251397
}
252398
}
253399

400+
/// Prepare test server params and spawn the ldk-server process.
401+
fn spawn_server(
402+
bitcoind: &TestBitcoind, config_fn: impl FnOnce(&TestServerParams) -> String,
403+
) -> (Child, TestServerParams, PathBuf) {
404+
#[allow(deprecated)]
405+
let storage_dir = tempfile::tempdir().unwrap().into_path();
406+
let grpc_port = find_available_port();
407+
let p2p_port = find_available_port();
408+
409+
let (rpc_host, rpc_port_num, rpc_user, rpc_password) = bitcoind.rpc_details();
410+
let rpc_address = format!("{rpc_host}:{rpc_port_num}");
411+
412+
let params =
413+
TestServerParams { grpc_port, p2p_port, storage_dir, rpc_address, rpc_user, rpc_password };
414+
415+
let config_content = config_fn(&params);
416+
417+
let config_path = params.storage_dir.join("config.toml");
418+
std::fs::write(&config_path, &config_content).unwrap();
419+
420+
let server_binary = server_binary_path();
421+
let child = Command::new(&server_binary)
422+
.arg(config_path.to_str().unwrap())
423+
.stdout(Stdio::piped())
424+
.stderr(Stdio::piped())
425+
.spawn()
426+
.unwrap_or_else(|e| {
427+
panic!("Failed to start ldk-server binary at {:?}: {}", server_binary, e)
428+
});
429+
430+
(child, params, config_path)
431+
}
432+
433+
/// Start ldk-server with the given config and expect it to fail (exit non-zero).
434+
/// Returns the stderr output for assertion in tests.
435+
pub fn start_expect_failure(
436+
bitcoind: &TestBitcoind, config_fn: impl FnOnce(&TestServerParams) -> String,
437+
) -> String {
438+
let (mut child, ..) = spawn_server(bitcoind, config_fn);
439+
440+
let timeout = Duration::from_secs(30);
441+
let start = std::time::Instant::now();
442+
loop {
443+
match child.try_wait() {
444+
Ok(Some(_)) => break,
445+
Ok(None) => {
446+
if start.elapsed() > timeout {
447+
let _ = child.kill();
448+
panic!(
449+
"Server did not exit within {:?} — it may have started successfully \
450+
instead of failing",
451+
timeout
452+
);
453+
}
454+
std::thread::sleep(Duration::from_millis(100));
455+
},
456+
Err(e) => panic!("Failed to wait for ldk-server process: {}", e),
457+
}
458+
}
459+
460+
let output = child
461+
.wait_with_output()
462+
.unwrap_or_else(|e| panic!("Failed to read ldk-server output: {}", e));
463+
464+
assert!(
465+
!output.status.success(),
466+
"Expected server to fail but it exited with status: {}",
467+
output.status
468+
);
469+
470+
String::from_utf8_lossy(&output.stderr).to_string()
471+
}
254472
/// Find an available TCP port by binding to port 0.
255473
pub fn find_available_port() -> u16 {
256474
let listener = TcpListener::bind("127.0.0.1:0").unwrap();

0 commit comments

Comments
 (0)