From aa2fd1a4bf7b8bc4eaccaa92e088df31fd8faf86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zoe=20Faltib=C3=A0?= Date: Thu, 12 Sep 2024 17:33:10 +0200 Subject: [PATCH] add a stress test --- .gitignore | 3 +- README.md | 36 ++++++++++++++-- tests/start_services.sh | 4 +- tests/stress.rs | 92 +++++++++++++++++++++++++++++++++++++++++ tests/transfers.rs | 22 +++++++--- tests/utils/chain.rs | 8 ++-- tests/utils/helpers.rs | 80 ++++++++++++++++++++++++++++------- tests/utils/mod.rs | 6 +++ tests/validation.rs | 3 ++ 9 files changed, 222 insertions(+), 32 deletions(-) create mode 100644 tests/stress.rs diff --git a/.gitignore b/.gitignore index e562211..83921c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ **/target -**/tests/tmp -**/tests/mainnet /.vim +/test-data diff --git a/README.md b/README.md index eea974d..5e595f9 100644 --- a/README.md +++ b/README.md @@ -19,15 +19,45 @@ Note: after checking out to another commit, remember to run: git submodule update ``` -Then, from the project root, run the tests by running: +### Integration tests + +To run the integration tests, from the project root, execute: ```sh -cargo test +cargo test --test issuance --test transfers ``` :warning: **Warning:** if your machine has a lot of CPU cores, it could happen that calls to indexers fail because of too many parallel requests. To limit the test threads and avoid this issue set the `--test-threads` option -(e.g. `cargo test -- --test-threads=8`). +(e.g. `cargo test --test issuance --test transfers -- --test-threads=8`). + +### Validation tests + +To run consignment validation tests, from the project root, execute: + +```sh +cargo test --test validation +``` + +### Stress tests + +To run a single stress test, set the `LOOPS` variable to the requested number +of loops and then, from the project root, for example execute: +```sh +LOOPS=20 cargo test --test stress back_and_forth::case_1 -- --ignored +``` + +This will produce a CSV report file that can be opened in a spreadsheet program +manually or by running: +```sh +open test-data/stress/.csv +``` + +Stress tests have been parametrized the same way some integration tests are. +To select which test case you want to run, find the case attribute you want to +use (e.g. `#[case(TT::Witness, DT::Wpkh, DT::Tr)]`) and if, as an example, it's +the 4th one, run `::case_4`. Note that case numbers are zero-padded +so if for example there are 20 test cases, case 4 would be called `case_04`. ### Test services diff --git a/tests/start_services.sh b/tests/start_services.sh index 5141597..3f6d93b 100755 --- a/tests/start_services.sh +++ b/tests/start_services.sh @@ -14,7 +14,7 @@ fi COMPOSE_BASE="$COMPOSE_BASE -f tests/docker-compose.yml" PROFILE=${PROFILE:-"esplora"} COMPOSE="$COMPOSE_BASE --profile $PROFILE" -TEST_DIR="./tests/tmp" +TEST_DATA_DIR="./test-data" # see docker-compose.yml for the exposed ports if [ "$PROFILE" == "esplora" ]; then @@ -29,7 +29,7 @@ fi # restart services (down + up) checking for ports availability $COMPOSE_BASE --profile '*' down -v --remove-orphans -mkdir -p $TEST_DIR +mkdir -p $TEST_DATA_DIR for port in "${EXPOSED_PORTS[@]}"; do if [ -n "$(ss -HOlnt "sport = :$port")" ];then _die "port $port is already bound, services can't be started" diff --git a/tests/stress.rs b/tests/stress.rs new file mode 100644 index 0000000..73161ff --- /dev/null +++ b/tests/stress.rs @@ -0,0 +1,92 @@ +pub mod utils; + +use utils::*; + +type TT = TransferType; +type DT = DescriptorType; + +#[rstest] +// blinded +#[case(TT::Blinded, DT::Wpkh, DT::Wpkh)] +#[case(TT::Blinded, DT::Wpkh, DT::Tr)] +#[case(TT::Blinded, DT::Tr, DT::Tr)] +// witness +#[case(TT::Witness, DT::Wpkh, DT::Wpkh)] +#[case(TT::Witness, DT::Wpkh, DT::Tr)] +#[case(TT::Witness, DT::Tr, DT::Tr)] +#[ignore = "run a single case if desired"] +fn back_and_forth( + #[case] transfer_type: TransferType, + #[case] wlt_1_desc: DescriptorType, + #[case] wlt_2_desc: DescriptorType, +) { + println!("transfer_type {transfer_type:?} wlt_1_desc {wlt_1_desc:?} wlt_2_desc {wlt_2_desc:?}"); + + initialize(); + + let stress_tests_dir = PathBuf::from(TEST_DATA_DIR).join(STRESS_DATA_DIR); + std::fs::create_dir_all(&stress_tests_dir).unwrap(); + let fname = OffsetDateTime::unix_timestamp(OffsetDateTime::now_utc()).to_string(); + let mut fpath = stress_tests_dir.join(fname); + fpath.set_extension("csv"); + println!("report path: {}", fpath.to_string_lossy()); + let report = Report { report_path: fpath }; + report.write_header(&[ + "wlt_1_pay", + "wlt_2_validate", + "wlt_2_accept", + "wlt_2_pay", + "wlt_1_validate", + "wlt_1_accept", + "send_1_tot", + "send_2_tot", + ]); + + let mut wlt_1 = get_wallet(&wlt_1_desc); + let mut wlt_2 = get_wallet(&wlt_2_desc); + + let issued_supply = u64::MAX; + + let (contract_id, iface_type_name) = wlt_1.issue_nia(issued_supply, wlt_1.close_method(), None); + + let loops = match std::env::var("LOOPS") { + Ok(val) if u16::from_str(&val).is_ok() => u16::from_str(&val).unwrap(), + Err(VarError::NotPresent) => 50, + _ => { + panic!("invalid loops value: must be a u16 number") + } + }; + + let now = Instant::now(); + for i in 1..=loops { + println!("loop {i}/{loops}"); + let wlt_1_send_start = Instant::now(); + wlt_1.send( + &mut wlt_2, + transfer_type, + contract_id, + &iface_type_name, + issued_supply - i as u64, + 1000, + Some(&report), + ); + let wlt_1_send_duration = wlt_1_send_start.elapsed(); + let wlt_2_send_start = Instant::now(); + wlt_2.send( + &mut wlt_1, + transfer_type, + contract_id, + &iface_type_name, + issued_supply - i as u64 - 1, + 1000, + Some(&report), + ); + let wlt_2_send_duration = wlt_2_send_start.elapsed(); + + report.write_duration(wlt_1_send_duration); + report.write_duration(wlt_2_send_duration); + report.end_line(); + } + let elapsed = now.elapsed(); + println!("elapsed: {:.2?}", elapsed); +} diff --git a/tests/transfers.rs b/tests/transfers.rs index f817dd7..4a0efa4 100644 --- a/tests/transfers.rs +++ b/tests/transfers.rs @@ -159,6 +159,7 @@ fn transfer_loop( &iface_type_name_1, amount_1, sats, + None, ); wlt_1.check_allocations( contract_id_1, @@ -192,6 +193,7 @@ fn transfer_loop( &iface_type_name_1, amount_2, sats, + None, ); wlt_1.check_allocations( contract_id_1, @@ -229,6 +231,7 @@ fn transfer_loop( &iface_type_name_2, amount_3, sats, + None, ); wlt_1.check_allocations( contract_id_1, @@ -273,6 +276,7 @@ fn transfer_loop( &iface_type_name_1, amount_4, sats, + None, ); wlt_1.check_allocations( contract_id_1, @@ -317,6 +321,7 @@ fn transfer_loop( &iface_type_name_2, amount_5, sats, + None, ); wlt_1.check_allocations( contract_id_1, @@ -361,6 +366,7 @@ fn transfer_loop( &iface_type_name_1, amount_6, sats, + None, ); wlt_1.check_allocations( contract_id_1, @@ -405,6 +411,7 @@ fn transfer_loop( &iface_type_name_2, amount_7, sats, + None, ); wlt_1.check_allocations( contract_id_1, @@ -457,13 +464,13 @@ fn same_transfer_twice() { wlt_2.close_method(), InvoiceType::Witness, ); - let _ = wlt_1.transfer(invoice.clone(), None, Some(500)); + let _ = wlt_1.transfer(invoice.clone(), None, Some(500), None); // retry with higher fees, TX hasn't been mined let mid_height = get_height(); assert_eq!(initial_height, mid_height); - let _ = wlt_1.transfer(invoice, None, Some(1000)); + let _ = wlt_1.transfer(invoice, None, Some(1000), None); let final_height = get_height(); assert_eq!(initial_height, final_height); @@ -488,9 +495,9 @@ fn accept_0conf() { wlt_2.close_method(), InvoiceType::Witness, ); - let (consignment, _) = wlt_1.transfer(invoice.clone(), None, None); + let (consignment, _) = wlt_1.transfer(invoice.clone(), None, None, None); - wlt_2.accept_transfer(consignment.clone()); + wlt_2.accept_transfer(consignment.clone(), None); // TODO: check if it's correct that sender sees 2 allocations /* @@ -692,6 +699,7 @@ fn ln_transfers() { &iface_type_name, 500, 1000, + None, ); } @@ -714,7 +722,7 @@ fn mainnet_wlt_receiving_test_asset() { wlt_2.close_method(), InvoiceType::Blinded(Some(utxo)), ); - let (consignment, tx) = wlt_1.transfer(invoice.clone(), None, Some(500)); + let (consignment, tx) = wlt_1.transfer(invoice.clone(), None, Some(500), None); wlt_1.mine_tx(&tx.txid(), false); match consignment.validate(&wlt_2.get_resolver(), wlt_2.testnet()) { Err((status, _invalid_consignment)) => { @@ -745,6 +753,7 @@ fn tapret_wlt_receiving_opret() { &iface_type_name, 400, 5000, + None, ); println!("2nd transfer"); @@ -755,7 +764,7 @@ fn tapret_wlt_receiving_opret() { CloseMethod::OpretFirst, InvoiceType::Witness, ); - wlt_2.send_to_invoice(&mut wlt_1, invoice, None, None); + wlt_2.send_to_invoice(&mut wlt_1, invoice, None, None, None); println!("3rd transfer"); wlt_1.send( @@ -765,5 +774,6 @@ fn tapret_wlt_receiving_opret() { &iface_type_name, 300, 1000, + None, ); } diff --git a/tests/utils/chain.rs b/tests/utils/chain.rs index ed54e81..d824d4b 100644 --- a/tests/utils/chain.rs +++ b/tests/utils/chain.rs @@ -124,7 +124,7 @@ pub fn mine(resume: bool) { if mined { break; } - std::thread::sleep(std::time::Duration::from_millis(500)); + std::thread::sleep(Duration::from_millis(500)); } } @@ -141,7 +141,7 @@ pub fn mine_but_no_resume() { break; } drop(miner); - std::thread::sleep(std::time::Duration::from_millis(500)); + std::thread::sleep(Duration::from_millis(500)); } } @@ -162,7 +162,7 @@ pub fn stop_mining_when_alone() { break; } drop(miner); - std::thread::sleep(std::time::Duration::from_millis(500)); + std::thread::sleep(Duration::from_millis(500)); } } @@ -195,7 +195,7 @@ fn _wait_indexer_sync() { let t_0 = OffsetDateTime::now_utc(); let blockcount = get_height(); loop { - std::thread::sleep(std::time::Duration::from_millis(100)); + std::thread::sleep(Duration::from_millis(100)); match INDEXER.get().unwrap() { Indexer::Electrum => { let electrum_client = diff --git a/tests/utils/helpers.rs b/tests/utils/helpers.rs index 0c4ab50..a4cf28e 100644 --- a/tests/utils/helpers.rs +++ b/tests/utils/helpers.rs @@ -339,6 +339,40 @@ impl AssetInfo { } } +pub struct Report { + pub report_path: PathBuf, +} + +impl Report { + pub fn write_header(&self, fields: &[&str]) { + let mut file = std::fs::File::options() + .read(true) + .write(true) + .create_new(true) + .open(&self.report_path) + .unwrap(); + file.write_all(format!("{}\n", fields.join(";")).as_bytes()) + .unwrap(); + } + + pub fn write_duration(&self, duration: Duration) { + let mut file = OpenOptions::new() + .append(true) + .open(&self.report_path) + .unwrap(); + file.write_all(format!("{};", duration.as_millis()).as_bytes()) + .unwrap(); + } + + pub fn end_line(&self) { + let mut file = OpenOptions::new() + .append(true) + .open(&self.report_path) + .unwrap(); + file.write_all("\n".as_bytes()).unwrap(); + } +} + fn _get_wallet( descriptor_type: &DescriptorType, network: Network, @@ -418,7 +452,9 @@ pub fn get_wallet(descriptor_type: &DescriptorType) -> TestWallet { let xpriv_account = XprivAccount::with_seed(true, &seed).derive(h![86, 1, 0]); let fingerprint = xpriv_account.account_fp().to_string(); - let wallet_dir = PathBuf::from("tests").join("tmp").join(fingerprint); + let wallet_dir = PathBuf::from(TEST_DATA_DIR) + .join(INTEGRATION_DATA_DIR) + .join(fingerprint); _get_wallet( descriptor_type, @@ -433,7 +469,9 @@ pub fn get_mainnet_wallet() -> TestWallet { "[c32338a7/86h/0h/0h]xpub6CmiK1xc7YwL472qm4zxeURFX8yMCSasioXujBjVMMzA3AKZr6KLQEmkzDge1Ezn2p43ZUysyx6gfajFVVnhtQ1AwbXEHrioLioXXgj2xW5" ).unwrap(); - let wallet_dir = PathBuf::from("tests").join("mainnet"); + let wallet_dir = PathBuf::from(TEST_DATA_DIR) + .join(INTEGRATION_DATA_DIR) + .join("mainnet"); _get_wallet( &DescriptorType::Wpkh, @@ -767,13 +805,19 @@ impl TestWallet { invoice: RgbInvoice, sats: Option, fee: Option, + report: Option<&Report>, ) -> (Transfer, Tx) { self.sync(); let fee = Sats::from_sats(fee.unwrap_or(400)); let sats = Sats::from_sats(sats.unwrap_or(2000)); let params = TransferParams::with(fee, sats); + let pay_start = Instant::now(); let (mut psbt, _psbt_meta, consignment) = self.wallet.pay(&invoice, params).unwrap(); + let pay_duration = pay_start.elapsed(); + if let Some(report) = report { + report.write_duration(pay_duration); + } let mut cs_path = self.wallet_dir.join("consignments"); std::fs::create_dir_all(&cs_path).unwrap(); @@ -811,24 +855,27 @@ impl TestWallet { (consignment, tx) } - pub fn accept_transfer(&mut self, consignment: Transfer) { + pub fn accept_transfer(&mut self, consignment: Transfer, report: Option<&Report>) { self.sync(); let resolver = self.get_resolver(); + let validate_start = Instant::now(); let validated_consignment = consignment.validate(&resolver, self.testnet()).unwrap(); + let validate_duration = validate_start.elapsed(); + if let Some(report) = report { + report.write_duration(validate_duration); + } + let validation_status = validated_consignment.clone().into_validation_status(); let validity = validation_status.validity(); assert_eq!(validity, Validity::Valid); - let mut attempts = 0; - while let Err(e) = self - .wallet + let accept_start = Instant::now(); + self.wallet .stock_mut() .accept_transfer(validated_consignment.clone(), &resolver) - { - attempts += 1; - if attempts > 3 { - panic!("error accepting transfer: {e}"); - } - std::thread::sleep(std::time::Duration::from_millis(100)); + .unwrap(); + let accept_duration = accept_start.elapsed(); + if let Some(report) = report { + report.write_duration(accept_duration); } } @@ -928,6 +975,7 @@ impl TestWallet { println!("\nWallet total balance: {} ṩ", bp_runtime.balance()); } + #[allow(clippy::too_many_arguments)] pub fn send( &mut self, recv_wlt: &mut TestWallet, @@ -936,6 +984,7 @@ impl TestWallet { iface_type_name: &TypeName, amount: u64, sats: u64, + report: Option<&Report>, ) -> (Transfer, Tx) { let invoice = match transfer_type { TransferType::Blinded => recv_wlt.invoice( @@ -953,7 +1002,7 @@ impl TestWallet { InvoiceType::Witness, ), }; - self.send_to_invoice(recv_wlt, invoice, Some(sats), None) + self.send_to_invoice(recv_wlt, invoice, Some(sats), None, report) } pub fn send_to_invoice( @@ -962,10 +1011,11 @@ impl TestWallet { invoice: RgbInvoice, sats: Option, fee: Option, + report: Option<&Report>, ) -> (Transfer, Tx) { - let (consignment, tx) = self.transfer(invoice, sats, fee); + let (consignment, tx) = self.transfer(invoice, sats, fee, report); self.mine_tx(&tx.txid(), false); - recv_wlt.accept_transfer(consignment.clone()); + recv_wlt.accept_transfer(consignment.clone(), report); self.sync(); (consignment, tx) } diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 18c037f..dc3e5d8 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -1,6 +1,10 @@ pub mod chain; pub mod helpers; +pub const TEST_DATA_DIR: &str = "test-data"; +pub const INTEGRATION_DATA_DIR: &str = "integration"; +pub const STRESS_DATA_DIR: &str = "stress"; + pub const ELECTRUM_REGTEST_URL: &str = "127.0.0.1:50001"; pub const ELECTRUM_MAINNET_URL: &str = "ssl://electrum.iriswallet.com:50003"; pub const ESPLORA_REGTEST_URL: &str = "http://127.0.0.1:8094/regtest/api"; @@ -14,11 +18,13 @@ pub use std::{ env::VarError, ffi::OsString, fmt::{self, Display}, + fs::OpenOptions, io::Write, path::{PathBuf, MAIN_SEPARATOR}, process::{Command, Stdio}, str::FromStr, sync::{Mutex, Once, OnceLock, RwLock}, + time::{Duration, Instant}, }; pub use amplify::{ diff --git a/tests/validation.rs b/tests/validation.rs index 7605a19..c43df13 100644 --- a/tests/validation.rs +++ b/tests/validation.rs @@ -137,6 +137,7 @@ fn get_consignment(scenario: Scenario) -> (Transfer, Vec) { &iface_type_name_1, 66, sats, + None, ); txes.push(tx); @@ -148,6 +149,7 @@ fn get_consignment(scenario: Scenario) -> (Transfer, Vec) { &iface_type_name_2, 50, sats, + None, ); txes.push(tx); @@ -159,6 +161,7 @@ fn get_consignment(scenario: Scenario) -> (Transfer, Vec) { &iface_type_name_2, 77, sats, + None, ); txes.push(tx);