diff --git a/Cargo.lock b/Cargo.lock index f238fc2f..ba2b0c5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2389,7 +2389,6 @@ dependencies = [ "common", "indexer_service", "indexer_service_rpc", - "integration_tests", "jsonrpsee", "nssa", "sequencer_service", @@ -2397,6 +2396,7 @@ dependencies = [ "serde", "serde_json", "tempfile", + "test_fixtures", "tokio", "wallet", ] @@ -4016,6 +4016,7 @@ dependencies = [ "sequencer_service_rpc", "serde_json", "tempfile", + "test_fixtures", "testcontainers", "token_core", "tokio", @@ -9221,6 +9222,34 @@ dependencies = [ "test-case-core", ] +[[package]] +name = "test_fixtures" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytesize", + "common", + "env_logger", + "futures", + "indexer_service", + "jsonrpsee", + "key_protocol", + "log", + "nssa", + "nssa_core", + "sequencer_core", + "sequencer_service", + "sequencer_service_rpc", + "serde", + "serde_json", + "tempfile", + "testcontainers", + "tokio", + "url", + "vault_core", + "wallet", +] + [[package]] name = "test_program_methods" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index d75e26c0..169e0cb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ members = [ "examples/program_deployment/methods/guest", "testnet_initial_state", "indexer/ffi", + "test_fixtures", "tools/cycle_bench", "tools/crypto_primitives_bench", "tools/e2e_bench", @@ -77,6 +78,7 @@ vault_core = { path = "programs/vault/core" } test_program_methods = { path = "test_program_methods" } testnet_initial_state = { path = "testnet_initial_state" } integration_tests = { path = "integration_tests" } +test_fixtures = { path = "test_fixtures" } tokio = { version = "1.50", features = [ "net", diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 536f30bc..04cd8f8c 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -8,6 +8,8 @@ license = { workspace = true } workspace = true [dependencies] +test_fixtures.workspace = true + nssa_core = { workspace = true, features = ["host"] } nssa.workspace = true authenticated_transfer_core.workspace = true diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 3662e006..d3fa7c64 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -1,441 +1,6 @@ -//! This library contains common code for integration tests. +//! Integration test helpers, re-exported from `test_fixtures` for backwards +//! compatibility. The actual fixtures live in the `test_fixtures` crate so that +//! non-test consumers (e.g. `integration_bench`) can depend on them without +//! pulling in the test files. -use std::{net::SocketAddr, sync::LazyLock}; - -use anyhow::{Context as _, Result}; -use common::{HashType, transaction::NSSATransaction}; -use futures::FutureExt as _; -use indexer_service::IndexerHandle; -use log::{debug, error}; -use nssa::{AccountId, PrivacyPreservingTransaction}; -use nssa_core::Commitment; -use sequencer_core::config::GenesisAction; -use sequencer_service::SequencerHandle; -use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; -use tempfile::TempDir; -use testcontainers::compose::DockerCompose; -use wallet::{WalletCore, account::AccountIdWithPrivacy, cli::CliAccountMention}; - -use crate::{ - indexer_client::IndexerClient, - setup::{ - setup_bedrock_node, setup_indexer, setup_private_accounts_with_initial_supply, - setup_public_accounts_with_initial_supply, setup_sequencer, setup_wallet, - }, -}; - -pub mod config; -pub mod indexer_client; -pub mod setup; - -// TODO: Remove this and control time from tests -pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12; -pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &str = "data_changer.bin"; -pub const NSSA_PROGRAM_FOR_TEST_NOOP: &str = "noop.bin"; -pub const NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY: &str = "pda_fund_spend_proxy.bin"; - -const BEDROCK_SERVICE_WITH_OPEN_PORT: &str = "logos-blockchain-node-0"; -const BEDROCK_SERVICE_PORT: u16 = 18080; - -static LOGGER: LazyLock<()> = LazyLock::new(env_logger::init); - -struct IndexerComponents { - indexer_handle: IndexerHandle, - indexer_client: IndexerClient, - _temp_dir: TempDir, -} - -impl Drop for IndexerComponents { - fn drop(&mut self) { - let Self { - indexer_handle, - indexer_client: _, - _temp_dir: _, - } = self; - - if !indexer_handle.is_healthy() { - error!("Indexer handle has unexpectedly stopped before IndexerComponents drop"); - } - } -} - -/// Test context which sets up a sequencer and a wallet for integration tests. -/// -/// It's memory and logically safe to create multiple instances of this struct in parallel tests, -/// as each instance uses its own temporary directories for sequencer and wallet data. -// NOTE: Order of fields is important for proper drop order. -pub struct TestContext { - sequencer_client: SequencerClient, - wallet: WalletCore, - wallet_password: String, - /// Optional to move out value in Drop. - sequencer_handle: Option, - indexer_components: Option, - bedrock_compose: DockerCompose, - bedrock_addr: SocketAddr, - _temp_sequencer_dir: TempDir, - _temp_wallet_dir: TempDir, -} - -impl TestContext { - /// Create new test context. - pub async fn new() -> Result { - Self::builder().build().await - } - - /// Get a builder for the test context to customize its configuration. - #[must_use] - pub const fn builder() -> TestContextBuilder { - TestContextBuilder::new() - } - - /// Get reference to the wallet. - #[must_use] - pub const fn wallet(&self) -> &WalletCore { - &self.wallet - } - - #[must_use] - pub fn wallet_password(&self) -> &str { - &self.wallet_password - } - - /// Get mutable reference to the wallet. - pub const fn wallet_mut(&mut self) -> &mut WalletCore { - &mut self.wallet - } - - /// Get reference to the sequencer client. - #[must_use] - pub const fn sequencer_client(&self) -> &SequencerClient { - &self.sequencer_client - } - - /// Get the Bedrock Node address. - #[must_use] - pub const fn bedrock_addr(&self) -> SocketAddr { - self.bedrock_addr - } - - /// Get reference to the indexer. - /// - /// # Panics - /// - /// Panics if the indexer is not enabled in the test context. See - /// [`TestContextBuilder::disable_indexer()`]. - #[must_use] - pub fn indexer(&self) -> &IndexerHandle { - self.indexer_components - .as_ref() - .map(|components| &components.indexer_handle) - .expect("Called `TestContext::indexer()` on context with disabled indexer") - } - - /// Get reference to the indexer client. - /// - /// # Panics - /// - /// Panics if the indexer is not enabled in the test context. See - /// [`TestContextBuilder::disable_indexer()`]. - #[must_use] - pub fn indexer_client(&self) -> &IndexerClient { - self.indexer_components - .as_ref() - .map(|components| &components.indexer_client) - .expect("Called `TestContext::indexer_client()` on context with disabled indexer") - } - - /// Get existing public account IDs in the wallet. - #[must_use] - pub fn existing_public_accounts(&self) -> Vec { - self.wallet - .storage() - .key_chain() - .public_account_ids() - .map(|(account_id, _idx)| account_id) - .collect() - } - - /// Get existing private account IDs in the wallet. - #[must_use] - pub fn existing_private_accounts(&self) -> Vec { - self.wallet - .storage() - .key_chain() - .private_account_ids() - .map(|(account_id, _idx)| account_id) - .collect() - } -} - -impl Drop for TestContext { - fn drop(&mut self) { - let Self { - sequencer_handle, - bedrock_compose, - bedrock_addr: _, - indexer_components: _, - sequencer_client: _, - wallet: _, - wallet_password: _, - _temp_sequencer_dir: _, - _temp_wallet_dir: _, - } = self; - - let sequencer_handle = sequencer_handle - .take() - .expect("Sequencer handle should be present in TestContext drop"); - if !sequencer_handle.is_healthy() { - let Err(err) = sequencer_handle - .failed() - .now_or_never() - .expect("Sequencer handle should not be running"); - error!( - "Sequencer handle has unexpectedly stopped before TestContext drop with error: {err:#}" - ); - } - - let container = bedrock_compose - .service(BEDROCK_SERVICE_WITH_OPEN_PORT) - .unwrap_or_else(|| { - panic!("Failed to get Bedrock service container `{BEDROCK_SERVICE_WITH_OPEN_PORT}`") - }); - let output = std::process::Command::new("docker") - .args(["inspect", "-f", "{{.State.Running}}", container.id()]) - .output() - .expect("Failed to execute docker inspect command to check if Bedrock container is still running"); - let stdout = String::from_utf8(output.stdout) - .expect("Failed to parse docker inspect output as String"); - if stdout.trim() != "true" { - error!( - "Bedrock container `{}` is not running during TestContext drop, docker inspect output: {stdout}", - container.id() - ); - } - } -} - -pub struct TestContextBuilder { - genesis_transactions: Option>, - sequencer_partial_config: Option, - enable_indexer: bool, -} - -impl TestContextBuilder { - const fn new() -> Self { - Self { - genesis_transactions: None, - sequencer_partial_config: None, - enable_indexer: true, - } - } - - #[must_use] - pub fn with_genesis(mut self, genesis_transactions: Vec) -> Self { - self.genesis_transactions = Some(genesis_transactions); - self - } - - #[must_use] - pub const fn with_sequencer_partial_config( - mut self, - sequencer_partial_config: config::SequencerPartialConfig, - ) -> Self { - self.sequencer_partial_config = Some(sequencer_partial_config); - self - } - - /// Exclude Indexer from test context. - /// Indexer is enabled by default. - /// - /// Methods like [`TestContext::indexer()`] and [`TestContext::indexer_client()`] will panic if - /// called when indexer is disabled. - #[must_use] - pub const fn disable_indexer(mut self) -> Self { - self.enable_indexer = false; - self - } - - pub async fn build(self) -> Result { - let Self { - genesis_transactions, - sequencer_partial_config, - enable_indexer, - } = self; - - // Ensure logger is initialized only once - *LOGGER; - - debug!("Test context setup"); - - let (bedrock_compose, bedrock_addr) = setup_bedrock_node() - .await - .context("Failed to setup Bedrock node")?; - - let indexer_components = if enable_indexer { - let (indexer_handle, temp_indexer_dir) = setup_indexer(bedrock_addr) - .await - .context("Failed to setup Indexer")?; - let indexer_url = config::addr_to_url(config::UrlProtocol::Ws, indexer_handle.addr()) - .context("Failed to convert indexer addr to URL")?; - let indexer_client = IndexerClient::new(&indexer_url) - .await - .context("Failed to create indexer client")?; - Some(IndexerComponents { - indexer_handle, - indexer_client, - _temp_dir: temp_indexer_dir, - }) - } else { - None - }; - - let initial_public_accounts = config::default_public_accounts_for_wallet(); - let initial_private_accounts = config::default_private_accounts_for_wallet(); - let (sequencer_handle, temp_sequencer_dir) = setup_sequencer( - sequencer_partial_config.unwrap_or_default(), - bedrock_addr, - genesis_transactions.unwrap_or_else(|| { - config::genesis_from_accounts(&initial_public_accounts, &initial_private_accounts) - }), - ) - .await - .context("Failed to setup Sequencer")?; - - let (mut wallet, temp_wallet_dir, wallet_password) = setup_wallet( - sequencer_handle.addr(), - &initial_public_accounts, - &initial_private_accounts, - ) - .context("Failed to setup wallet")?; - - setup_public_accounts_with_initial_supply(&wallet, &initial_public_accounts) - .await - .context("Failed to initialize public accounts in wallet")?; - - setup_private_accounts_with_initial_supply(&mut wallet, &initial_private_accounts) - .await - .context("Failed to initialize private accounts in wallet")?; - - let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_handle.addr()) - .context("Failed to convert sequencer addr to URL")?; - let sequencer_client = SequencerClientBuilder::default() - .build(sequencer_url) - .context("Failed to create sequencer client")?; - - Ok(TestContext { - sequencer_client, - wallet, - wallet_password, - bedrock_compose, - bedrock_addr, - sequencer_handle: Some(sequencer_handle), - indexer_components, - _temp_sequencer_dir: temp_sequencer_dir, - _temp_wallet_dir: temp_wallet_dir, - }) - } - - pub fn build_blocking(self) -> Result { - let runtime = tokio::runtime::Runtime::new().context("Failed to create Tokio runtime")?; - - let ctx = runtime.block_on(self.build())?; - - Ok(BlockingTestContext { - ctx: Some(ctx), - runtime, - }) - } -} -/// A test context to be used in normal #[test] tests. -pub struct BlockingTestContext { - ctx: Option, - runtime: tokio::runtime::Runtime, -} - -impl BlockingTestContext { - pub fn new() -> Result { - TestContext::builder().build_blocking() - } - - pub const fn ctx(&self) -> &TestContext { - self.ctx.as_ref().expect("TestContext is set") - } - - pub const fn runtime(&self) -> &tokio::runtime::Runtime { - &self.runtime - } - - pub fn block_on<'ctx, F>(&'ctx self, f: impl FnOnce(&'ctx TestContext) -> F) -> F::Output - where - F: std::future::Future + 'ctx, - { - let future = f(self.ctx()); - self.runtime.block_on(future) - } - - pub fn block_on_mut<'ctx, F>( - &'ctx mut self, - f: impl FnOnce(&'ctx mut TestContext) -> F, - ) -> F::Output - where - F: std::future::Future + 'ctx, - { - let ctx_mut = self.ctx.as_mut().expect("TestContext is set"); - let future = f(ctx_mut); - self.runtime.block_on(future) - } -} - -impl Drop for BlockingTestContext { - fn drop(&mut self) { - let Self { ctx, runtime } = self; - - // Ensure async cleanup of TestContext by blocking on its drop in the runtime. - runtime.block_on(async { - if let Some(ctx) = ctx.take() { - drop(ctx); - } - }); - } -} - -#[must_use] -pub const fn public_mention(account_id: AccountId) -> CliAccountMention { - CliAccountMention::Id(AccountIdWithPrivacy::Public(account_id)) -} - -#[must_use] -pub const fn private_mention(account_id: AccountId) -> CliAccountMention { - CliAccountMention::Id(AccountIdWithPrivacy::Private(account_id)) -} - -#[expect( - clippy::wildcard_enum_match_arm, - reason = "We want the code to panic if the transaction type is not PrivacyPreserving" -)] -pub async fn fetch_privacy_preserving_tx( - seq_client: &SequencerClient, - tx_hash: HashType, -) -> PrivacyPreservingTransaction { - let tx = seq_client.get_transaction(tx_hash).await.unwrap().unwrap(); - - match tx { - NSSATransaction::PrivacyPreserving(privacy_preserving_transaction) => { - privacy_preserving_transaction - } - _ => panic!("Invalid tx type"), - } -} - -pub async fn verify_commitment_is_in_state( - commitment: Commitment, - seq_client: &SequencerClient, -) -> bool { - seq_client - .get_proof_for_commitment(commitment) - .await - .ok() - .flatten() - .is_some() -} +pub use test_fixtures::*; diff --git a/test_fixtures/Cargo.toml b/test_fixtures/Cargo.toml new file mode 100644 index 00000000..1bfd2284 --- /dev/null +++ b/test_fixtures/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "test_fixtures" +version = "0.1.0" +edition = "2024" +license = { workspace = true } +publish = false + +[lints] +workspace = true + +[dependencies] +common.workspace = true +indexer_service.workspace = true +key_protocol.workspace = true +nssa.workspace = true +nssa_core = { workspace = true, features = ["host"] } +sequencer_core = { workspace = true, features = ["default", "testnet"] } +sequencer_service.workspace = true +sequencer_service_rpc = { workspace = true, features = ["client"] } +vault_core.workspace = true +wallet.workspace = true + +anyhow.workspace = true +bytesize.workspace = true +env_logger.workspace = true +futures.workspace = true +jsonrpsee = { workspace = true, features = ["ws-client"] } +log.workspace = true +serde.workspace = true +serde_json.workspace = true +tempfile.workspace = true +testcontainers = { version = "0.27.3", features = ["docker-compose"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +url.workspace = true diff --git a/integration_tests/src/config.rs b/test_fixtures/src/config.rs similarity index 100% rename from integration_tests/src/config.rs rename to test_fixtures/src/config.rs diff --git a/integration_tests/src/indexer_client.rs b/test_fixtures/src/indexer_client.rs similarity index 100% rename from integration_tests/src/indexer_client.rs rename to test_fixtures/src/indexer_client.rs diff --git a/test_fixtures/src/lib.rs b/test_fixtures/src/lib.rs new file mode 100644 index 00000000..da2b7be2 --- /dev/null +++ b/test_fixtures/src/lib.rs @@ -0,0 +1,500 @@ +//! Shared test/bench fixtures: spins up bedrock + sequencer + indexer + wallet +//! end-to-end against docker-compose, exposes a `TestContext` callers can drive. +//! +//! Originally lived under `integration_tests`; split out so non-test consumers +//! (e.g. `integration_bench`) can depend on the fixtures without pulling in the +//! `integration_tests` test files. + +use std::{net::SocketAddr, path::Path, sync::LazyLock}; + +use anyhow::{Context as _, Result}; +use common::{HashType, transaction::NSSATransaction}; +use futures::FutureExt as _; +use indexer_service::IndexerHandle; +use log::{debug, error}; +use nssa::{AccountId, PrivacyPreservingTransaction}; +use nssa_core::Commitment; +use sequencer_core::config::GenesisAction; +use sequencer_service::SequencerHandle; +use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; +use serde::Serialize; +use tempfile::TempDir; +use testcontainers::compose::DockerCompose; +use wallet::{WalletCore, account::AccountIdWithPrivacy, cli::CliAccountMention}; + +use crate::{ + indexer_client::IndexerClient, + setup::{ + setup_bedrock_node, setup_indexer, setup_private_accounts_with_initial_supply, + setup_public_accounts_with_initial_supply, setup_sequencer, setup_wallet, + }, +}; + +pub mod config; +pub mod indexer_client; +pub mod setup; + +// TODO: Remove this and control time from tests +pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12; +pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &str = "data_changer.bin"; +pub const NSSA_PROGRAM_FOR_TEST_NOOP: &str = "noop.bin"; +pub const NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY: &str = "pda_fund_spend_proxy.bin"; + +pub(crate) const BEDROCK_SERVICE_WITH_OPEN_PORT: &str = "logos-blockchain-node-0"; +pub(crate) const BEDROCK_SERVICE_PORT: u16 = 18080; + +static LOGGER: LazyLock<()> = LazyLock::new(env_logger::init); + +struct IndexerComponents { + indexer_handle: IndexerHandle, + indexer_client: IndexerClient, + temp_dir: TempDir, +} + +impl Drop for IndexerComponents { + fn drop(&mut self) { + let Self { + indexer_handle, + indexer_client: _, + temp_dir: _, + } = self; + + if !indexer_handle.is_healthy() { + error!("Indexer handle has unexpectedly stopped before IndexerComponents drop"); + } + } +} + +/// Recursively-sized bytes on disk for sequencer / indexer / wallet tempdirs. +#[derive(Debug, Clone, Copy, Default, Serialize)] +pub struct DiskSizes { + pub sequencer_bytes: u64, + pub indexer_bytes: u64, + pub wallet_bytes: u64, +} + +/// Test context which sets up a sequencer and a wallet for integration tests. +/// +/// It's memory and logically safe to create multiple instances of this struct in parallel tests, +/// as each instance uses its own temporary directories for sequencer and wallet data. +// NOTE: Order of fields is important for proper drop order. +pub struct TestContext { + sequencer_client: SequencerClient, + wallet: WalletCore, + wallet_password: String, + /// Optional to move out value in Drop. + sequencer_handle: Option, + indexer_components: Option, + bedrock_compose: DockerCompose, + bedrock_addr: SocketAddr, + temp_sequencer_dir: TempDir, + temp_wallet_dir: TempDir, +} + +impl TestContext { + /// Create new test context. + pub async fn new() -> Result { + Self::builder().build().await + } + + /// Get a builder for the test context to customize its configuration. + #[must_use] + pub const fn builder() -> TestContextBuilder { + TestContextBuilder::new() + } + + /// Get reference to the wallet. + #[must_use] + pub const fn wallet(&self) -> &WalletCore { + &self.wallet + } + + #[must_use] + pub fn wallet_password(&self) -> &str { + &self.wallet_password + } + + /// Get mutable reference to the wallet. + pub const fn wallet_mut(&mut self) -> &mut WalletCore { + &mut self.wallet + } + + /// Get reference to the sequencer client. + #[must_use] + pub const fn sequencer_client(&self) -> &SequencerClient { + &self.sequencer_client + } + + /// Get the Bedrock Node address. + #[must_use] + pub const fn bedrock_addr(&self) -> SocketAddr { + self.bedrock_addr + } + + /// Get reference to the indexer. + /// + /// # Panics + /// + /// Panics if the indexer is not enabled in the test context. See + /// [`TestContextBuilder::disable_indexer()`]. + #[must_use] + pub fn indexer(&self) -> &IndexerHandle { + self.indexer_components + .as_ref() + .map(|components| &components.indexer_handle) + .expect("Called `TestContext::indexer()` on context with disabled indexer") + } + + /// Get the indexer's bound socket address. + /// + /// # Panics + /// + /// Panics if the indexer is not enabled in the test context. + #[must_use] + pub fn indexer_addr(&self) -> SocketAddr { + self.indexer().addr() + } + + /// Get reference to the indexer client. + /// + /// # Panics + /// + /// Panics if the indexer is not enabled in the test context. See + /// [`TestContextBuilder::disable_indexer()`]. + #[must_use] + pub fn indexer_client(&self) -> &IndexerClient { + self.indexer_components + .as_ref() + .map(|components| &components.indexer_client) + .expect("Called `TestContext::indexer_client()` on context with disabled indexer") + } + + /// Recursively-sized bytes on disk for sequencer + indexer + wallet tempdirs. + /// Indexer bytes are zero if the indexer is disabled. + #[must_use] + pub fn disk_sizes(&self) -> DiskSizes { + DiskSizes { + sequencer_bytes: dir_size_bytes(self.temp_sequencer_dir.path()), + indexer_bytes: self + .indexer_components + .as_ref() + .map_or(0, |c| dir_size_bytes(c.temp_dir.path())), + wallet_bytes: dir_size_bytes(self.temp_wallet_dir.path()), + } + } + + /// Get existing public account IDs in the wallet. + #[must_use] + pub fn existing_public_accounts(&self) -> Vec { + self.wallet + .storage() + .key_chain() + .public_account_ids() + .map(|(account_id, _idx)| account_id) + .collect() + } + + /// Get existing private account IDs in the wallet. + #[must_use] + pub fn existing_private_accounts(&self) -> Vec { + self.wallet + .storage() + .key_chain() + .private_account_ids() + .map(|(account_id, _idx)| account_id) + .collect() + } +} + +impl Drop for TestContext { + fn drop(&mut self) { + let Self { + sequencer_handle, + bedrock_compose, + bedrock_addr: _, + indexer_components: _, + sequencer_client: _, + wallet: _, + wallet_password: _, + temp_sequencer_dir: _, + temp_wallet_dir: _, + } = self; + + let sequencer_handle = sequencer_handle + .take() + .expect("Sequencer handle should be present in TestContext drop"); + if !sequencer_handle.is_healthy() { + let Err(err) = sequencer_handle + .failed() + .now_or_never() + .expect("Sequencer handle should not be running"); + error!( + "Sequencer handle has unexpectedly stopped before TestContext drop with error: {err:#}" + ); + } + + let container = bedrock_compose + .service(BEDROCK_SERVICE_WITH_OPEN_PORT) + .unwrap_or_else(|| { + panic!("Failed to get Bedrock service container `{BEDROCK_SERVICE_WITH_OPEN_PORT}`") + }); + let output = std::process::Command::new("docker") + .args(["inspect", "-f", "{{.State.Running}}", container.id()]) + .output() + .expect("Failed to execute docker inspect command to check if Bedrock container is still running"); + let stdout = String::from_utf8(output.stdout) + .expect("Failed to parse docker inspect output as String"); + if stdout.trim() != "true" { + error!( + "Bedrock container `{}` is not running during TestContext drop, docker inspect output: {stdout}", + container.id() + ); + } + } +} + +pub struct TestContextBuilder { + genesis_transactions: Option>, + sequencer_partial_config: Option, + enable_indexer: bool, +} + +impl TestContextBuilder { + const fn new() -> Self { + Self { + genesis_transactions: None, + sequencer_partial_config: None, + enable_indexer: true, + } + } + + #[must_use] + pub fn with_genesis(mut self, genesis_transactions: Vec) -> Self { + self.genesis_transactions = Some(genesis_transactions); + self + } + + #[must_use] + pub const fn with_sequencer_partial_config( + mut self, + sequencer_partial_config: config::SequencerPartialConfig, + ) -> Self { + self.sequencer_partial_config = Some(sequencer_partial_config); + self + } + + /// Exclude Indexer from test context. + /// Indexer is enabled by default. + /// + /// Methods like [`TestContext::indexer()`] and [`TestContext::indexer_client()`] will panic if + /// called when indexer is disabled. + #[must_use] + pub const fn disable_indexer(mut self) -> Self { + self.enable_indexer = false; + self + } + + pub async fn build(self) -> Result { + let Self { + genesis_transactions, + sequencer_partial_config, + enable_indexer, + } = self; + + // Ensure logger is initialized only once + *LOGGER; + + debug!("Test context setup"); + + let (bedrock_compose, bedrock_addr) = setup_bedrock_node() + .await + .context("Failed to setup Bedrock node")?; + + let indexer_components = if enable_indexer { + let (indexer_handle, temp_indexer_dir) = setup_indexer(bedrock_addr) + .await + .context("Failed to setup Indexer")?; + let indexer_url = config::addr_to_url(config::UrlProtocol::Ws, indexer_handle.addr()) + .context("Failed to convert indexer addr to URL")?; + let indexer_client = IndexerClient::new(&indexer_url) + .await + .context("Failed to create indexer client")?; + Some(IndexerComponents { + indexer_handle, + indexer_client, + temp_dir: temp_indexer_dir, + }) + } else { + None + }; + + let initial_public_accounts = config::default_public_accounts_for_wallet(); + let initial_private_accounts = config::default_private_accounts_for_wallet(); + let (sequencer_handle, temp_sequencer_dir) = setup_sequencer( + sequencer_partial_config.unwrap_or_default(), + bedrock_addr, + genesis_transactions.unwrap_or_else(|| { + config::genesis_from_accounts(&initial_public_accounts, &initial_private_accounts) + }), + ) + .await + .context("Failed to setup Sequencer")?; + + let (mut wallet, temp_wallet_dir, wallet_password) = setup_wallet( + sequencer_handle.addr(), + &initial_public_accounts, + &initial_private_accounts, + ) + .context("Failed to setup wallet")?; + + setup_public_accounts_with_initial_supply(&wallet, &initial_public_accounts) + .await + .context("Failed to initialize public accounts in wallet")?; + + setup_private_accounts_with_initial_supply(&mut wallet, &initial_private_accounts) + .await + .context("Failed to initialize private accounts in wallet")?; + + let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_handle.addr()) + .context("Failed to convert sequencer addr to URL")?; + let sequencer_client = SequencerClientBuilder::default() + .build(sequencer_url) + .context("Failed to create sequencer client")?; + + Ok(TestContext { + sequencer_client, + wallet, + wallet_password, + bedrock_compose, + bedrock_addr, + sequencer_handle: Some(sequencer_handle), + indexer_components, + temp_sequencer_dir, + temp_wallet_dir, + }) + } + + pub fn build_blocking(self) -> Result { + let runtime = tokio::runtime::Runtime::new().context("Failed to create Tokio runtime")?; + + let ctx = runtime.block_on(self.build())?; + + Ok(BlockingTestContext { + ctx: Some(ctx), + runtime, + }) + } +} +/// A test context to be used in normal #[test] tests. +pub struct BlockingTestContext { + ctx: Option, + runtime: tokio::runtime::Runtime, +} + +impl BlockingTestContext { + pub fn new() -> Result { + TestContext::builder().build_blocking() + } + + pub const fn ctx(&self) -> &TestContext { + self.ctx.as_ref().expect("TestContext is set") + } + + pub const fn runtime(&self) -> &tokio::runtime::Runtime { + &self.runtime + } + + pub fn block_on<'ctx, F>(&'ctx self, f: impl FnOnce(&'ctx TestContext) -> F) -> F::Output + where + F: std::future::Future + 'ctx, + { + let future = f(self.ctx()); + self.runtime.block_on(future) + } + + pub fn block_on_mut<'ctx, F>( + &'ctx mut self, + f: impl FnOnce(&'ctx mut TestContext) -> F, + ) -> F::Output + where + F: std::future::Future + 'ctx, + { + let ctx_mut = self.ctx.as_mut().expect("TestContext is set"); + let future = f(ctx_mut); + self.runtime.block_on(future) + } +} + +impl Drop for BlockingTestContext { + fn drop(&mut self) { + let Self { ctx, runtime } = self; + + // Ensure async cleanup of TestContext by blocking on its drop in the runtime. + runtime.block_on(async { + if let Some(ctx) = ctx.take() { + drop(ctx); + } + }); + } +} + +#[must_use] +pub const fn public_mention(account_id: AccountId) -> CliAccountMention { + CliAccountMention::Id(AccountIdWithPrivacy::Public(account_id)) +} + +#[must_use] +pub const fn private_mention(account_id: AccountId) -> CliAccountMention { + CliAccountMention::Id(AccountIdWithPrivacy::Private(account_id)) +} + +#[expect( + clippy::wildcard_enum_match_arm, + reason = "We want the code to panic if the transaction type is not PrivacyPreserving" +)] +pub async fn fetch_privacy_preserving_tx( + seq_client: &SequencerClient, + tx_hash: HashType, +) -> PrivacyPreservingTransaction { + let tx = seq_client.get_transaction(tx_hash).await.unwrap().unwrap(); + + match tx { + NSSATransaction::PrivacyPreserving(privacy_preserving_transaction) => { + privacy_preserving_transaction + } + _ => panic!("Invalid tx type"), + } +} + +pub async fn verify_commitment_is_in_state( + commitment: Commitment, + seq_client: &SequencerClient, +) -> bool { + seq_client + .get_proof_for_commitment(commitment) + .await + .ok() + .flatten() + .is_some() +} + +fn dir_size_bytes(path: &Path) -> u64 { + let mut total = 0_u64; + let Ok(entries) = std::fs::read_dir(path) else { + return 0; + }; + for entry in entries.flatten() { + let Ok(metadata) = entry.metadata() else { + continue; + }; + if metadata.is_file() { + total = total.saturating_add(metadata.len()); + } else if metadata.is_dir() { + total = total.saturating_add(dir_size_bytes(&entry.path())); + } else { + // Sockets, FIFOs, block/char devices: ignore. Symlinks are + // already followed by `is_file()` / `is_dir()`. + } + } + total +} diff --git a/integration_tests/src/setup.rs b/test_fixtures/src/setup.rs similarity index 100% rename from integration_tests/src/setup.rs rename to test_fixtures/src/setup.rs diff --git a/tools/e2e_bench/Cargo.toml b/tools/e2e_bench/Cargo.toml index ab6a6eb0..97d34f53 100644 --- a/tools/e2e_bench/Cargo.toml +++ b/tools/e2e_bench/Cargo.toml @@ -12,10 +12,10 @@ workspace = true common.workspace = true indexer_service.workspace = true indexer_service_rpc = { workspace = true, features = ["client"] } -integration_tests.workspace = true nssa.workspace = true sequencer_service.workspace = true sequencer_service_rpc = { workspace = true, features = ["client"] } +test_fixtures.workspace = true wallet.workspace = true anyhow.workspace = true diff --git a/tools/e2e_bench/src/bench_context.rs b/tools/e2e_bench/src/bench_context.rs index 41f0d59d..e3de508e 100644 --- a/tools/e2e_bench/src/bench_context.rs +++ b/tools/e2e_bench/src/bench_context.rs @@ -15,7 +15,7 @@ use std::{env, net::SocketAddr, path::Path}; use anyhow::{Context as _, Result}; use indexer_service::IndexerHandle; -use integration_tests::config::{ +use test_fixtures::config::{ SequencerPartialConfig, UrlProtocol, addr_to_url, default_private_accounts_for_wallet, default_public_accounts_for_wallet, genesis_from_accounts, indexer_config, sequencer_config, wallet_config, diff --git a/tools/e2e_bench/src/scenarios/amm.rs b/tools/e2e_bench/src/scenarios/amm.rs index 6756321d..f295a4aa 100644 --- a/tools/e2e_bench/src/scenarios/amm.rs +++ b/tools/e2e_bench/src/scenarios/amm.rs @@ -3,7 +3,7 @@ use std::time::Instant; use anyhow::{Result, bail}; -use integration_tests::public_mention; +use test_fixtures::public_mention; use wallet::cli::{ Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand}, diff --git a/tools/e2e_bench/src/scenarios/fanout.rs b/tools/e2e_bench/src/scenarios/fanout.rs index 59e9a64b..6f85a974 100644 --- a/tools/e2e_bench/src/scenarios/fanout.rs +++ b/tools/e2e_bench/src/scenarios/fanout.rs @@ -3,7 +3,7 @@ use std::time::Instant; use anyhow::{Result, bail}; -use integration_tests::public_mention; +use test_fixtures::public_mention; use wallet::cli::{ Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand}, diff --git a/tools/e2e_bench/src/scenarios/parallel.rs b/tools/e2e_bench/src/scenarios/parallel.rs index 86368a0d..29c3a72f 100644 --- a/tools/e2e_bench/src/scenarios/parallel.rs +++ b/tools/e2e_bench/src/scenarios/parallel.rs @@ -7,7 +7,7 @@ use std::time::Instant; use anyhow::{Result, bail}; use common::transaction::NSSATransaction; -use integration_tests::public_mention; +use test_fixtures::public_mention; use sequencer_service_rpc::RpcClient as _; use wallet::cli::{ Command, SubcommandReturnValue, diff --git a/tools/e2e_bench/src/scenarios/private.rs b/tools/e2e_bench/src/scenarios/private.rs index c6ef9888..f0b4745b 100644 --- a/tools/e2e_bench/src/scenarios/private.rs +++ b/tools/e2e_bench/src/scenarios/private.rs @@ -3,7 +3,7 @@ use std::time::Instant; use anyhow::{Result, bail}; -use integration_tests::{private_mention, public_mention}; +use test_fixtures::{private_mention, public_mention}; use wallet::cli::{ Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand}, diff --git a/tools/e2e_bench/src/scenarios/token.rs b/tools/e2e_bench/src/scenarios/token.rs index 24c38fc3..4e63da32 100644 --- a/tools/e2e_bench/src/scenarios/token.rs +++ b/tools/e2e_bench/src/scenarios/token.rs @@ -3,7 +3,7 @@ use std::time::Instant; use anyhow::{Result, bail}; -use integration_tests::{private_mention, public_mention}; +use test_fixtures::{private_mention, public_mention}; use wallet::cli::{ Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand},