From 3dbbc85eac400f564c28d54697e152825df4637d Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 12 Oct 2023 16:54:44 -0400 Subject: [PATCH] multi: add test utilities to help mocking lightning nodes --- Cargo.lock | 97 ++++++++++++++++++++++++++++++++++++++++ sim-lib/Cargo.toml | 1 + sim-lib/src/lib.rs | 108 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 206 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 8296001a..5a850805 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -443,6 +443,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "dirs" version = "1.0.5" @@ -454,6 +460,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "either" version = "1.9.0" @@ -522,12 +534,27 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "futures" version = "0.3.28" @@ -939,6 +966,33 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "mockall" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "mpsc" version = "0.2.0" @@ -972,6 +1026,12 @@ dependencies = [ "libc", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "num-traits" version = "0.2.16" @@ -1103,6 +1163,36 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "prettyplease" version = "0.1.25" @@ -1612,6 +1702,7 @@ dependencies = [ "hex", "lightning", "log", + "mockall", "mpsc", "rand", "rand_distr", @@ -1736,6 +1827,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" version = "1.0.48" diff --git a/sim-lib/Cargo.toml b/sim-lib/Cargo.toml index 87ea691d..56756f41 100644 --- a/sim-lib/Cargo.toml +++ b/sim-lib/Cargo.toml @@ -29,3 +29,4 @@ hex = "0.4.3" csv = "1.2.2" serde_millis = "0.1.1" rand_distr = "0.4.3" +mockall = "0.11.4" diff --git a/sim-lib/src/lib.rs b/sim-lib/src/lib.rs index e559edf1..a42b5e48 100644 --- a/sim-lib/src/lib.rs +++ b/sim-lib/src/lib.rs @@ -1076,3 +1076,111 @@ async fn track_payment_result( log::trace!("Payment result tracker exiting."); } + +#[cfg(test)] +mod tests { + use crate::{LightningError, LightningNode, NodeInfo, PaymentResult, Simulation}; + use async_trait::async_trait; + use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + use bitcoin::Network; + use lightning::ln::features::NodeFeatures; + use lightning::ln::PaymentHash; + use mockall::mock; + use std::collections::HashMap; + use std::sync::Arc; + use tokio::sync::Mutex; + use triggered::Listener; + + mock! { + LnNode{} + + #[async_trait] + impl LightningNode for LnNode { + fn get_info(&self) -> &NodeInfo; + async fn get_network(&mut self) -> Result; + async fn send_payment(&mut self,dest: PublicKey,amount_msat: u64) -> Result; + async fn track_payment(&mut self,hash: PaymentHash,shutdown: Listener,) -> Result; + async fn get_node_info(&mut self, node_id: &PublicKey) -> Result; + async fn list_channels(&mut self) -> Result, LightningError>; + } + } + + /// pubkey produces a deterministic test public key with a private key based off the byte value provided. + fn pubkey(byte: u8) -> PublicKey { + let secp_ctx = Secp256k1::new(); + PublicKey::from_secret_key(&secp_ctx, &privkey(42 + byte)) + } + + /// privkey produces a deterministic test private key based off the byte value passed. + fn privkey(byte: u8) -> SecretKey { + SecretKey::from_slice(&[byte; 32]).unwrap() + } + + /// primes each mocked node to return its info when get_info is called. + async fn mock_get_info(nodes: HashMap>>) { + for (pubkey, node) in nodes.iter() { + let mut features = NodeFeatures::empty(); + features.set_keysend_optional(); + + node.lock().await.expect_get_info().return_const(NodeInfo { + pubkey: *pubkey, + alias: "".to_string(), + features, + }); + } + } + + async fn mock_get_network(nodes: HashMap>>) { + for (_, node) in nodes.iter() { + node.lock() + .await + .expect_get_network() + .returning(|| Ok(Network::Regtest)); + } + } + + /// new_simulation_test sets up a test simulation with the desired node count. It returns the simulation object + /// and a map of public keys to mocked lightning nodes (which can be used to assert the expectations of the test). + fn new_simulation_test( + node_count: u8, + ) -> (Simulation, HashMap>>) { + assert_ne!(0, node_count, "at least one node required for tests"); + + // For each node, create a public key and a mocked lightning node implementation. + let mut nodes: HashMap>> = HashMap::new(); + let mut mock_nodes = HashMap::new(); + + for i in 0..node_count { + let node_pubkey = pubkey(i); + let mock = Arc::new(Mutex::new(MockLnNode::new())); + + nodes.insert(node_pubkey, mock.clone()); + mock_nodes.insert(node_pubkey, mock); + } + + ( + Simulation::new(nodes, vec![], None, None, None, None), + mock_nodes, + ) + } + + #[tokio::test] + async fn example_test() { + let (simulation, mocks) = new_simulation_test(3); + let shutdown = simulation.shutdown_trigger.clone(); + + // Prime the mock to return the function calls that we check during validation. + mock_get_info(mocks.clone()).await; + mock_get_network(mocks).await; + + // TODO: need to mock out list_channels etc (called to start random activity). + // TODO: mock out generation traits so that we can control payments etc (will require a refactor to set mocks). + + let run_task = tokio::spawn(async move { simulation.run().await }); + + // test logic goes here! + + shutdown.trigger(); + let _ = run_task.await.expect("task did not exit cleanly"); + } +}