Skip to content

Commit

Permalink
feat(test): implementing a new test framework for core lightning
Browse files Browse the repository at this point in the history
Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
  • Loading branch information
vincenzopalazzo committed Jul 5, 2023
1 parent 5460467 commit d7f5048
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"plugin",
"plugin_macros",
"conf",
"testing",
]
resolver = "2"

13 changes: 13 additions & 0 deletions testing/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "testing"
version = "0.1.0"
edition = "2021"

[dependencies]
clightningrpc = "0.3.0-beta.6"
bitcoincore-rpc = "0.17.0"
log = "0.4.19"
tempfile = "3.6.0"
port-selector = "0.1.6"
anyhow = "1.0.71"
tokio = { version = "1.22.0", features = ["process", "time", "fs"] }
108 changes: 108 additions & 0 deletions testing/src/btc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//! Bitcoin Testing framework.
use bitcoincore_rpc::{Auth, Client, RpcApi};
use port::Port;
use port_selector as port;
use tempfile::TempDir;

pub mod macros {
#[macro_export]
macro_rules! bitcoind {
($dir:expr, $port:expr, $opt_args:expr) => {
async {
use std::process::Stdio;

use log;
use tokio::process::Command;

let opt_args = format!($opt_args);
let args = opt_args.trim();
let args_tok: Vec<&str> = args.split(" ").collect();
log::debug!("additional args: {:?}", args_tok);
let mut command = Command::new("bitcoind");
command
.args(&args_tok)
.arg(format!("-port={}", $port + 1))
.arg(format!("-rpcport={}", $port))
.arg(format!("-datadir={}", $dir.path().to_str().unwrap()))
.stdout(Stdio::null())
.spawn()
}
.await
};
($dir:expr, $port:expr) => {
$crate::bitcoind!($dir, $port, "")
};
}

pub use bitcoind;
}

pub struct BtcNode {
inner: Client,
pub user: String,
pub pass: String,
pub port: Port,
root_path: TempDir,
process: Vec<tokio::process::Child>,
}

impl Drop for BtcNode {
fn drop(&mut self) {
for process in self.process.iter() {
let Some(child) = process.id() else {
continue;
};
let Ok(mut kill) = std::process::Command::new("kill")
.args(["-s", "SIGKILL", &child.to_string()])
.spawn() else {
continue;
};
let _ = kill.wait();
}

let result = std::fs::remove_dir_all(self.root_path.path());
log::debug!(target: "btc", "clean up function {:?}", result);
}
}

impl BtcNode {
pub async fn tmp() -> anyhow::Result<Self> {
let dir = tempfile::tempdir()?;
let user = "crab".to_owned();
let pass = "crab".to_owned();
let port = port::random_free_port().unwrap();
let process = macros::bitcoind!(
dir,
port,
"-server -regtest -rpcuser={user} -rpcpassword={pass}"
)?;
let rpc = Client::new(
&format!("http://localhost:{port}"),
Auth::UserPass(user.clone(), pass.clone()),
)?;
let bg_process = vec![process];
Ok(Self {
inner: rpc,
root_path: dir,
user,
pass,
port,
process: bg_process,
})
}

pub fn rpc(&self) -> &Client {
&self.inner
}

pub async fn stop(&mut self) -> anyhow::Result<()> {
log::info!("stop bitcoin node");
self.inner.stop()?;
for process in self.process.iter_mut() {
process.kill().await?;
let _ = process.wait().await?;
log::debug!("process killed");
}
Ok(())
}
}
114 changes: 114 additions & 0 deletions testing/src/cln.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//! Integration testing library for core lightning
use port_selector as port;
use tempfile::TempDir;

use clightningrpc::LightningRPC;

use crate::btc::BtcNode;
use crate::prelude::*;

pub mod macros {
#[macro_export]
macro_rules! lightningd {
($dir:expr, $port:expr, $($opt_args:tt)*) => {
async {
use std::process::Stdio;

use tokio::process::Command;

let opt_args = format!($($opt_args)*);
let args = opt_args.trim();
let args_tok: Vec<&str> = args.split(" ").collect();

let path = format!("{}/.lightning", $dir.path().to_str().unwrap());
log::info!("core lightning home {path}");
check_dir_or_make_if_missing(path.clone()).await.unwrap();
let mut command = Command::new("lightningd");
command
.args(&args_tok)
.arg(format!("--addr=127.0.0.1:{}", $port))
.arg(format!("--bind-addr=127.0.0.1:{}", $port))
.arg(format!("--lightning-dir={path}"))
.arg("--dev-fast-gossip")
.arg("--funding-confirms=1")
.stdout(Stdio::null())
.spawn()
}.await
};
($dir:expr, $port:expr) => {
$crate::lightningd!($dir, $port, "")
};
}

pub use lightningd;
}

pub struct Node {
inner: LightningRPC,
root_path: TempDir,
bitcoin: BtcNode,
process: Vec<tokio::process::Child>,
}

impl Drop for Node {
fn drop(&mut self) {
for process in self.process.iter() {
let Some(child) = process.id() else {
continue;
};
let Ok(mut kill) = std::process::Command::new("kill")
.args(["-s", "SIGKILL", &child.to_string()])
.spawn() else {
continue;
};
let _ = kill.wait();
}

let result = std::fs::remove_dir_all(self.root_path.path());
log::debug!(target: "cln", "clean up function {:?}", result);
}
}

impl Node {
pub async fn tmp() -> anyhow::Result<Self> {
let btc = BtcNode::tmp().await?;

let dir = tempfile::tempdir()?;

let process = macros::lightningd!(
dir,
port::random_free_port().unwrap(),
"--network=regtest --bitcoin-rpcuser={} --bitcoin-rpcpassword={} --bitcoin-rpcport={}",
btc.user,
btc.pass,
btc.port,
)?;

let rpc = LightningRPC::new(dir.path().join(".lightning/regtest").join("lightning-rpc"));

wait_for!(async { rpc.getinfo() });

Ok(Self {
inner: rpc,
root_path: dir,
bitcoin: btc,
process: vec![process],
})
}

pub fn rpc(&self) -> &LightningRPC {
&self.inner
}

pub async fn stop(&mut self) -> anyhow::Result<()> {
log::info!("stop lightning node");
self.inner.stop()?;
for process in self.process.iter_mut() {
process.kill().await?;
let _ = process.wait().await?;
log::debug!("killing process");
}
self.bitcoin.stop().await?;
Ok(())
}
}
50 changes: 50 additions & 0 deletions testing/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
pub mod btc;
pub mod cln;

pub mod prelude {
pub use crate::macros::*;
pub use port_selector as port;
pub use tempfile;

pub use crate::check_dir_or_make_if_missing;
}

pub mod macros {
#[macro_export]
macro_rules! wait_for {
($callback:expr, $timeout:expr) => {
use log;
use tokio::time::{sleep, Duration};

for wait in 0..$timeout {
if let Err(err) = $callback.await {
log::debug!("callback return {:?}", err);
sleep(Duration::from_millis(wait)).await;
continue;
}
log::info!("callback completed in {wait} milliseconds");
break;
}
};
($callback:expr) => {
use crate::DEFAULT_TIMEOUT;

Check warning on line 30 in testing/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

`crate` references the macro call's crate

warning: `crate` references the macro call's crate --> testing/src/lib.rs:30:17 | 30 | use crate::DEFAULT_TIMEOUT; | ^^^^^ help: to reference the macro definition's crate, use: `$crate` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#crate_in_macro_def = note: `#[warn(clippy::crate_in_macro_def)]` on by default

$crate::wait_for!($callback, DEFAULT_TIMEOUT);
};
}

pub use wait_for;
}

static DEFAULT_TIMEOUT: u64 = 100;

pub async fn check_dir_or_make_if_missing(path: String) -> anyhow::Result<()> {
use std::path::Path;
use tokio::fs::create_dir;

if !Path::exists(Path::new(&path.to_owned())) {
create_dir(path.clone()).await?;
log::debug!("created dir {path}");
}
Ok(())
}

0 comments on commit d7f5048

Please sign in to comment.