Skip to content

Commit df68774

Browse files
authored
Merge pull request #3016 from lightning-signer/2024-04-ext-test
allow functional tests to be used externally with a dynamic signer factory
2 parents 0216d7d + 9d8a91a commit df68774

38 files changed

+1191
-511
lines changed

.github/workflows/build.yml

+14
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ concurrency:
1313
cancel-in-progress: true
1414

1515
jobs:
16+
ext-test:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Checkout source code
20+
uses: actions/checkout@v4
21+
- name: Install Rust stable toolchain
22+
run: |
23+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain stable
24+
rustup override set stable
25+
- name: Run externalized tests
26+
run: |
27+
cd ext-functional-test-demo
28+
cargo test --verbose --color always
29+
cargo test --verbose --color always --features test-broken
1630
build:
1731
strategy:
1832
fail-fast: false

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ lightning-rapid-gossip-sync/res/full_graph.lngossip
1313
lightning-custom-message/target
1414
lightning-transaction-sync/target
1515
lightning-dns-resolver/target
16+
ext-functional-test-demo/target
1617
no-std-check/target
1718
msrv-no-dev-deps-check/target

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ members = [
2121

2222
exclude = [
2323
"lightning-transaction-sync",
24+
"ext-functional-test-demo",
2425
"no-std-check",
2526
"msrv-no-dev-deps-check",
2627
"bench",

ext-functional-test-demo/Cargo.toml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "ext-functional-tester"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[features]
7+
test-broken = []
8+
9+
[dependencies]
10+
lightning = { path = "../lightning", features = ["_externalize_tests"] }

ext-functional-test-demo/src/main.rs

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
fn main() {
2+
println!("{} tests were exported", lightning::get_xtests().len());
3+
}
4+
5+
#[cfg(test)]
6+
#[allow(unused)]
7+
mod tests {
8+
use lightning::ln::functional_tests::*;
9+
use lightning::util::dyn_signer::{DynKeysInterfaceTrait, DynSigner};
10+
use lightning::util::test_utils::{TestSignerFactory, SIGNER_FACTORY};
11+
use std::panic::catch_unwind;
12+
use std::sync::Arc;
13+
use std::time::Duration;
14+
15+
struct BrokenSignerFactory();
16+
17+
impl TestSignerFactory for BrokenSignerFactory {
18+
fn make_signer(
19+
&self, _seed: &[u8; 32], _now: Duration,
20+
) -> Box<dyn DynKeysInterfaceTrait<EcdsaSigner = DynSigner>> {
21+
panic!()
22+
}
23+
}
24+
25+
#[cfg(feature = "test-broken")]
26+
#[test]
27+
fn test_broken() {
28+
SIGNER_FACTORY.set(Arc::new(BrokenSignerFactory()));
29+
catch_unwind(|| fake_network_test()).unwrap_err();
30+
}
31+
32+
#[cfg(not(feature = "test-broken"))]
33+
#[test]
34+
fn test_default_one() {
35+
test_htlc_on_chain_success();
36+
}
37+
38+
#[cfg(not(feature = "test-broken"))]
39+
#[test]
40+
fn test_default_all() {
41+
let mut failed_tests = Vec::new();
42+
for test in lightning::get_xtests() {
43+
print!("Running test: {}", test.test_name);
44+
let mut pass = catch_unwind(|| (test.test_fn)()).is_ok();
45+
if test.should_panic {
46+
pass = !pass;
47+
}
48+
if !pass {
49+
failed_tests.push(test.test_name);
50+
}
51+
}
52+
if !failed_tests.is_empty() {
53+
println!("Failed tests:");
54+
for test in failed_tests.iter() {
55+
println!("- {}", test);
56+
}
57+
}
58+
println!("Done with {} failures", failed_tests.len());
59+
assert!(failed_tests.is_empty());
60+
}
61+
}

fuzz/src/chanmon_consistency.rs

+2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ use bitcoin::secp256k1::schnorr;
7979
use bitcoin::secp256k1::{self, Message, PublicKey, Scalar, Secp256k1, SecretKey};
8080

8181
use lightning::io::Cursor;
82+
use lightning::util::dyn_signer::DynSigner;
8283
use std::cmp::{self, Ordering};
8384
use std::mem;
8485
use std::sync::atomic;
@@ -375,6 +376,7 @@ impl SignerProvider for KeyProvider {
375376
channel_keys_id,
376377
);
377378
let revoked_commitment = self.make_enforcement_state_cell(keys.commitment_seed);
379+
let keys = DynSigner::new(keys);
378380
TestChannelSigner::new_with_revoked(keys, revoked_commitment, false)
379381
}
380382

fuzz/src/full_stack.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature};
7575
use bitcoin::secp256k1::schnorr;
7676
use bitcoin::secp256k1::{self, Message, PublicKey, Scalar, Secp256k1, SecretKey};
7777

78+
use lightning::util::dyn_signer::DynSigner;
7879
use std::cell::RefCell;
7980
use std::cmp;
8081
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
@@ -439,7 +440,7 @@ impl SignerProvider for KeyProvider {
439440
let ctr = channel_keys_id[0];
440441
let (inbound, state) = self.signer_state.borrow().get(&ctr).unwrap().clone();
441442
TestChannelSigner::new_with_revoked(
442-
if inbound {
443+
DynSigner::new(if inbound {
443444
InMemorySigner::new(
444445
&secp_ctx,
445446
SecretKey::from_slice(&[
@@ -509,7 +510,7 @@ impl SignerProvider for KeyProvider {
509510
channel_keys_id,
510511
channel_keys_id,
511512
)
512-
},
513+
}),
513514
state,
514515
false,
515516
)

lightning-background-processor/src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -2550,6 +2550,8 @@ mod tests {
25502550
failure: PathFailure::OnPath { network_update: None },
25512551
path: path.clone(),
25522552
short_channel_id: Some(scored_scid),
2553+
error_code: None,
2554+
error_data: None,
25532555
});
25542556
let event = $receive.expect("PaymentPathFailed not handled within deadline");
25552557
match event {
@@ -2567,6 +2569,8 @@ mod tests {
25672569
failure: PathFailure::OnPath { network_update: None },
25682570
path: path.clone(),
25692571
short_channel_id: None,
2572+
error_code: None,
2573+
error_data: None,
25702574
});
25712575
let event = $receive.expect("PaymentPathFailed not handled within deadline");
25722576
match event {

lightning-macros/Cargo.toml

+5-2
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ proc-macro = true
1818
[features]
1919

2020
[dependencies]
21-
syn = { version = "2.0.77", default-features = false, features = ["parsing", "printing", "proc-macro", "full"] }
22-
proc-macro2 = { version = "1.0.86", default-features = false, features = ["proc-macro"] }
21+
syn = { version = "2.0", default-features = false, features = ["parsing", "printing", "proc-macro", "full"] }
22+
proc-macro2 = { version = "1.0", default-features = false, features = ["proc-macro"] }
2323
quote = { version = "1.0", default-features = false, features = ["proc-macro"] }
2424

25+
[dev-dependencies]
26+
inventory = "0.3"
27+
2528
[lints]
2629
workspace = true

lightning-macros/src/lib.rs

+106
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ extern crate alloc;
2222

2323
use alloc::string::ToString;
2424
use proc_macro::{Delimiter, Group, TokenStream, TokenTree};
25+
use proc_macro2::TokenStream as TokenStream2;
2526
use quote::quote;
2627
use syn::spanned::Spanned;
2728
use syn::{parse, ImplItemFn, Token};
29+
use syn::{parse_macro_input, Item};
2830

2931
fn add_async_method(mut parsed: ImplItemFn) -> TokenStream {
3032
let output = quote! {
@@ -294,3 +296,107 @@ pub fn drop_legacy_field_definition(expr: TokenStream) -> TokenStream {
294296
let out = syn::Expr::Struct(st);
295297
quote! { #out }.into()
296298
}
299+
300+
/// An exposed test. This is a test that will run locally and also be
301+
/// made available to other crates that want to run it in their own context.
302+
///
303+
/// For example:
304+
/// ```rust
305+
/// use lightning_macros::xtest;
306+
///
307+
/// fn f1() {}
308+
///
309+
/// #[xtest(feature = "_externalize_tests")]
310+
/// pub fn test_f1() {
311+
/// f1();
312+
/// }
313+
/// ```
314+
///
315+
/// Which will include the module if we are testing or the `_test_utils` feature
316+
/// is on.
317+
#[proc_macro_attribute]
318+
pub fn xtest(attrs: TokenStream, item: TokenStream) -> TokenStream {
319+
let attrs = parse_macro_input!(attrs as TokenStream2);
320+
let input = parse_macro_input!(item as Item);
321+
322+
let expanded = match input {
323+
Item::Fn(item_fn) => {
324+
let (cfg_attr, submit_attr) = if attrs.is_empty() {
325+
(quote! { #[cfg_attr(test, test)] }, quote! { #[cfg(not(test))] })
326+
} else {
327+
(
328+
quote! { #[cfg_attr(test, test)] #[cfg(any(test, #attrs))] },
329+
quote! { #[cfg(all(not(test), #attrs))] },
330+
)
331+
};
332+
333+
// Check that the function doesn't take args and returns nothing
334+
if !item_fn.sig.inputs.is_empty()
335+
|| !matches!(item_fn.sig.output, syn::ReturnType::Default)
336+
{
337+
return syn::Error::new_spanned(
338+
item_fn.sig,
339+
"xtest functions must not take arguments and must return nothing",
340+
)
341+
.to_compile_error()
342+
.into();
343+
}
344+
345+
// Check for #[should_panic] attribute
346+
let should_panic =
347+
item_fn.attrs.iter().any(|attr| attr.path().is_ident("should_panic"));
348+
349+
let fn_name = &item_fn.sig.ident;
350+
let fn_name_str = fn_name.to_string();
351+
quote! {
352+
#cfg_attr
353+
#item_fn
354+
355+
// We submit the test to the inventory only if we're not actually testing
356+
#submit_attr
357+
inventory::submit! {
358+
crate::XTestItem {
359+
test_fn: #fn_name,
360+
test_name: #fn_name_str,
361+
should_panic: #should_panic,
362+
}
363+
}
364+
}
365+
},
366+
_ => {
367+
return syn::Error::new_spanned(
368+
input,
369+
"xtest can only be applied to functions or modules",
370+
)
371+
.to_compile_error()
372+
.into();
373+
},
374+
};
375+
376+
TokenStream::from(expanded)
377+
}
378+
379+
/// Collects all externalized tests marked with `#[xtest]`
380+
/// into a vector of `XTestItem`s. This vector can be
381+
/// retrieved by calling `get_xtests()`.
382+
#[proc_macro]
383+
pub fn xtest_inventory(_input: TokenStream) -> TokenStream {
384+
let expanded = quote! {
385+
/// An externalized test item, including the test function, name, and whether it is marked with `#[should_panic]`.
386+
pub struct XTestItem {
387+
pub test_fn: fn(),
388+
pub test_name: &'static str,
389+
pub should_panic: bool,
390+
}
391+
392+
inventory::collect!(XTestItem);
393+
394+
pub fn get_xtests() -> Vec<&'static XTestItem> {
395+
inventory::iter::<XTestItem>
396+
.into_iter()
397+
.collect()
398+
}
399+
};
400+
401+
TokenStream::from(expanded)
402+
}

lightning/Cargo.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ rustdoc-args = ["--cfg", "docsrs"]
1818
[features]
1919
# Internal test utilities exposed to other repo crates
2020
_test_utils = ["regex", "bitcoin/bitcoinconsensus", "lightning-types/_test_utils"]
21-
21+
_externalize_tests = ["inventory", "_test_utils"]
2222
# Allow signing of local transactions that may have been revoked or will be revoked, for functional testing (e.g. justice tx handling).
2323
# This is unsafe to use in production because it may result in the counterparty publishing taking our funds.
2424
unsafe_revoked_tx_signing = []
@@ -48,10 +48,12 @@ regex = { version = "1.5.6", optional = true }
4848
backtrace = { version = "0.3", optional = true }
4949

5050
libm = { version = "0.2", default-features = false }
51+
inventory = { version = "0.3", optional = true }
5152

5253
[dev-dependencies]
5354
regex = "1.5.6"
5455
lightning-types = { version = "0.3.0", path = "../lightning-types", features = ["_test_utils"] }
56+
lightning-macros = { path = "../lightning-macros" }
5557

5658
[dev-dependencies.bitcoin]
5759
version = "0.32.2"

lightning/src/chain/chainmonitor.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ where C::Target: chain::Filter,
469469
}
470470

471471

472-
#[cfg(test)]
472+
#[cfg(any(test, feature = "_test_utils"))]
473473
pub fn remove_monitor(&self, channel_id: &ChannelId) -> ChannelMonitor<ChannelSigner> {
474474
self.monitors.write().unwrap().remove(channel_id).unwrap().monitor
475475
}

0 commit comments

Comments
 (0)