diff --git a/Cargo.lock b/Cargo.lock index c312f12818..6e5f4bafbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10307,6 +10307,7 @@ dependencies = [ "cairo-lang-starknet-classes", "futures", "indexmap 2.6.0", + "infra_utils", "mempool_test_utils", "papyrus_common", "papyrus_consensus", diff --git a/crates/infra_utils/src/command.rs b/crates/infra_utils/src/command.rs new file mode 100644 index 0000000000..4a22cac174 --- /dev/null +++ b/crates/infra_utils/src/command.rs @@ -0,0 +1,28 @@ +use std::env; +use std::process::Command; + +use crate::path::project_path; + +#[cfg(test)] +#[path = "command_test.rs"] +mod command_test; + +/// Returns a shell command originating from the project root, with cargo environment variables +/// filtered out. +/// +/// # Arguments +/// * `command_name` - The shell command name. +/// +/// # Returns +/// * A [`std::process::Command`] object with the current directory set to the project root, and +/// cleared out cargo related environment variables. +pub fn create_shell_command(command_name: &str) -> Command { + let project_path = project_path().expect("Failed to get project path"); + let mut command = Command::new(command_name); + command.current_dir(&project_path); + // Filter out all CARGO_ environment variables. + env::vars().filter(|(key, _)| key.starts_with("CARGO_")).for_each(|(key, _)| { + command.env_remove(key); + }); + command +} diff --git a/crates/infra_utils/src/command_test.rs b/crates/infra_utils/src/command_test.rs new file mode 100644 index 0000000000..239b92475f --- /dev/null +++ b/crates/infra_utils/src/command_test.rs @@ -0,0 +1,12 @@ +use crate::command::create_shell_command; + +#[test] +fn create_shell_command_example() { + let mut ls_command = create_shell_command("ls"); + let output = ls_command.output().expect("Failed to execute command"); + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!(output.status.success()); + // Project root should contain the `crates` directory. + assert!(stdout.contains("crates")); +} diff --git a/crates/infra_utils/src/lib.rs b/crates/infra_utils/src/lib.rs index 4da9789237..f744151bf9 100644 --- a/crates/infra_utils/src/lib.rs +++ b/crates/infra_utils/src/lib.rs @@ -1 +1,2 @@ +pub mod command; pub mod path; diff --git a/crates/infra_utils/src/path.rs b/crates/infra_utils/src/path.rs index cfa2444a24..2a0da9b203 100644 --- a/crates/infra_utils/src/path.rs +++ b/crates/infra_utils/src/path.rs @@ -1,6 +1,6 @@ -use std::env; use std::path::{Path, PathBuf}; use std::sync::LazyLock; +use std::{env, fs}; use thiserror::Error; @@ -10,11 +10,8 @@ mod path_test; #[derive(Debug, Error)] pub enum PathResolutionError { - // TODO(Arni): Handle manifest dir not exist here? - #[error("No file exists at '{path}'")] - PathDoesNotExist { path: PathBuf }, - /// This error is raised when file existence can be neither confirmed nor denied. See - /// [`std::path::Path::try_exists`] for more information. + /// This error is raised when the file path does not exist, or when a non-final component in a + /// path is not a directory. See [`std::fs::canonicalize`] for more information. #[error(transparent)] IoError(#[from] std::io::Error), } @@ -23,6 +20,7 @@ pub enum PathResolutionError { static PATH_TO_CARGO_MANIFEST_DIR: LazyLock> = LazyLock::new(|| env::var("CARGO_MANIFEST_DIR").ok().map(|dir| Path::new(&dir).into())); +// TODO(Tsabary): should not be public. Use a getter instead. pub fn cargo_manifest_dir() -> Option { PATH_TO_CARGO_MANIFEST_DIR.clone() } @@ -34,16 +32,21 @@ pub fn cargo_manifest_dir() -> Option { /// * `relative_path` - A string slice representing the relative path from the project root. /// /// # Returns -/// * An absolute `PathBuf` representing the resolved path starting from the project root. +/// * A `PathBuf` representing the resolved path starting from the project root. pub fn resolve_project_relative_path(relative_path: &str) -> Result { let base_dir = path_of_project_root(); - let path = base_dir.join(relative_path); - if !path.try_exists()? { - return Err(PathResolutionError::PathDoesNotExist { path }); - } + let absolute_path = fs::canonicalize(path)?; + + Ok(absolute_path) +} - Ok(path) +/// Returns the absolute path of the project root directory. +/// +/// # Returns +/// * A `PathBuf` representing the path of the project root. +pub fn project_path() -> Result { + resolve_project_relative_path(".") } fn path_of_project_root() -> PathBuf { diff --git a/crates/infra_utils/src/path_test.rs b/crates/infra_utils/src/path_test.rs index 88d7f5ff23..f0b757ae82 100644 --- a/crates/infra_utils/src/path_test.rs +++ b/crates/infra_utils/src/path_test.rs @@ -1,4 +1,4 @@ -use crate::path::{path_of_project_root, resolve_project_relative_path, PathResolutionError}; +use crate::path::{path_of_project_root, resolve_project_relative_path}; // TODO: Add a test for PathResolutionError::IoError. #[test] @@ -8,11 +8,7 @@ fn resolve_project_relative_path_on_non_existent_path() { assert!(!expected_path.exists()); let result = resolve_project_relative_path(relative_path); - if let Err(PathResolutionError::PathDoesNotExist { path }) = result { - assert_eq!(path, expected_path); - } else { - panic!("Expected PathDoesNotExist error, got {:?}", result); - } + assert!(result.is_err(), "Expected an non-existent path error, got {:?}", result); } #[test] diff --git a/crates/starknet_integration_tests/Cargo.toml b/crates/starknet_integration_tests/Cargo.toml index 2a77b40534..f6e957d73d 100644 --- a/crates/starknet_integration_tests/Cargo.toml +++ b/crates/starknet_integration_tests/Cargo.toml @@ -16,6 +16,7 @@ blockifier.workspace = true cairo-lang-starknet-classes.workspace = true futures.workspace = true indexmap.workspace = true +infra_utils.workspace = true mempool_test_utils.workspace = true papyrus_common.workspace = true papyrus_consensus.workspace = true @@ -44,8 +45,14 @@ tempfile.workspace = true tokio.workspace = true tracing.workspace = true + [dev-dependencies] futures.workspace = true pretty_assertions.workspace = true rstest.workspace = true starknet_sequencer_infra.workspace = true + + +[[bin]] +name = "end_to_end_integration_test" +path = "src/bin/end_to_end_integration_test.rs" diff --git a/crates/starknet_integration_tests/tests/end_to_end_integration_test.rs b/crates/starknet_integration_tests/bin/end_to_end_integration_test.rs similarity index 78% rename from crates/starknet_integration_tests/tests/end_to_end_integration_test.rs rename to crates/starknet_integration_tests/bin/end_to_end_integration_test.rs index 1602e1f87c..e3f3abfb84 100644 --- a/crates/starknet_integration_tests/tests/end_to_end_integration_test.rs +++ b/crates/starknet_integration_tests/bin/end_to_end_integration_test.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use std::process::Stdio; use std::time::Duration; +use infra_utils::path::resolve_project_relative_path; use mempool_test_utils::starknet_api_test_utils::{AccountId, MultiAccountTransactionGenerator}; use papyrus_execution::execution_utils::get_nonce_at; use papyrus_storage::state::StateStorageReader; @@ -14,6 +15,7 @@ use starknet_api::state::StateNumber; use starknet_integration_tests::integration_test_setup::IntegrationTestSetup; use starknet_integration_tests::utils::{create_integration_test_tx_generator, send_account_txs}; use starknet_sequencer_infra::trace_util::configure_tracing; +use starknet_sequencer_node::test_utils::compilation::compile_node_result; use starknet_types_core::felt::Felt; use tokio::process::{Child, Command}; use tokio::task::{self, JoinHandle}; @@ -28,18 +30,28 @@ fn tx_generator() -> MultiAccountTransactionGenerator { // TODO(Tsabary): Move to a suitable util location. async fn spawn_node_child_task(node_config_path: PathBuf) -> Child { // Get the current working directory for the project - let project_path = env::current_dir().expect("Failed to get current directory").join("../.."); - - // TODO(Tsabary): Capture output to a log file, and present it in case of a failure. - // TODO(Tsabary): Change invocation from "cargo run" to separate compilation and invocation - // (build, and then invoke the binary). - Command::new("cargo") - .arg("run") - .arg("--bin") - .arg("starknet_sequencer_node") - .arg("--quiet") + // let project_path = env::current_dir().expect("Failed to get current + // directory").join("../.."); + + let compile_result = compile_node_result(); + info!("Compilation result {:?}.", compile_result); + + assert!(compile_result.is_ok(), "Compilation failed."); + // let compilation_result = Command::new("cargo") + // .arg("build") + // .arg("--bin") + // .arg("starknet_sequencer_node") + // .arg("--quiet") + // .current_dir(&project_path) + // .status().await; + + info!("Compiling the starknet_sequencer_node binary"); + let project_path = resolve_project_relative_path(".").expect("Failed to resolve project path"); + info!("project_path {:?}", project_path); + + // Run `cargo build` to compile the project + Command::new("target/debug/starknet_sequencer_node") .current_dir(&project_path) - .arg("--") .arg("--config_file") .arg(node_config_path.to_str().unwrap()) .stderr(Stdio::inherit()) @@ -47,6 +59,24 @@ async fn spawn_node_child_task(node_config_path: PathBuf) -> Child { .kill_on_drop(true) // Required for stopping the node when the handle is dropped. .spawn() .expect("Failed to spawn the sequencer node.") + + // // TODO(Tsabary): Capture output to a log file, and present it in case of a failure. + // // TODO(Tsabary): Change invocation from "cargo run" to separate compilation and invocation + // // (build, and then invoke the binary). + // Command::new("cargo") + // .arg("run") + // .arg("--bin") + // .arg("starknet_sequencer_node") + // .arg("--quiet") + // .current_dir(&project_path) + // .arg("--") + // .arg("--config_file") + // .arg(node_config_path.to_str().unwrap()) + // .stderr(Stdio::inherit()) + // .stdout(Stdio::null()) + // .kill_on_drop(true) // Required for stopping the node when the handle is dropped. + // .spawn() + // .expect("Failed to spawn the sequencer node.") } async fn spawn_run_node(node_config_path: PathBuf) -> JoinHandle<()> { diff --git a/crates/starknet_integration_tests/src/bin/end_to_end_integration_test.rs b/crates/starknet_integration_tests/src/bin/end_to_end_integration_test.rs new file mode 100644 index 0000000000..cdd72c3e62 --- /dev/null +++ b/crates/starknet_integration_tests/src/bin/end_to_end_integration_test.rs @@ -0,0 +1,211 @@ +use std::env; +use std::path::PathBuf; +use std::process::Stdio; +use std::time::Duration; + +use infra_utils::path::resolve_project_relative_path; +use mempool_test_utils::starknet_api_test_utils::{AccountId, MultiAccountTransactionGenerator}; +use papyrus_execution::execution_utils::get_nonce_at; +use papyrus_storage::state::StateStorageReader; +use papyrus_storage::StorageReader; +use starknet_api::block::BlockNumber; +use starknet_api::core::{ContractAddress, Nonce}; +use starknet_api::state::StateNumber; +use starknet_integration_tests::integration_test_setup::IntegrationTestSetup; +use starknet_integration_tests::utils::{create_integration_test_tx_generator, send_account_txs}; +use starknet_sequencer_infra::trace_util::configure_tracing; +use starknet_sequencer_node::test_utils::compilation::compile_node_result; +use starknet_types_core::felt::Felt; +use tokio::process::{Child, Command}; +use tokio::task::{self, JoinHandle}; +use tokio::time::interval; +use tracing::{error, info}; + + +// TODO(Tsabary): Move to a suitable util location. +async fn spawn_node_child_task(node_config_path: PathBuf) -> Child { + // Get the current working directory for the project + // let project_path = env::current_dir().expect("Failed to get current + // directory").join("../.."); + + let compile_result = compile_node_result(); + info!("Compilation result {:?}.", compile_result); + + assert!(compile_result.is_ok(), "Compilation failed."); + // let compilation_result = Command::new("cargo") + // .arg("build") + // .arg("--bin") + // .arg("starknet_sequencer_node") + // .arg("--quiet") + // .current_dir(&project_path) + // .status().await; + + info!("Compiling the starknet_sequencer_node binary"); + let project_path = resolve_project_relative_path(".").expect("Failed to resolve project path"); + info!("project_path {:?}", project_path); + + // Run `cargo build` to compile the project + Command::new("target/debug/starknet_sequencer_node") + .current_dir(&project_path) + .arg("--config_file") + .arg(node_config_path.to_str().unwrap()) + .stderr(Stdio::inherit()) + .stdout(Stdio::null()) + .kill_on_drop(true) // Required for stopping the node when the handle is dropped. + .spawn() + .expect("Failed to spawn the sequencer node.") + + // // TODO(Tsabary): Capture output to a log file, and present it in case of a failure. + // // TODO(Tsabary): Change invocation from "cargo run" to separate compilation and invocation + // // (build, and then invoke the binary). + // Command::new("cargo") + // .arg("run") + // .arg("--bin") + // .arg("starknet_sequencer_node") + // .arg("--quiet") + // .current_dir(&project_path) + // .arg("--") + // .arg("--config_file") + // .arg(node_config_path.to_str().unwrap()) + // .stderr(Stdio::inherit()) + // .stdout(Stdio::null()) + // .kill_on_drop(true) // Required for stopping the node when the handle is dropped. + // .spawn() + // .expect("Failed to spawn the sequencer node.") +} + +async fn spawn_run_node(node_config_path: PathBuf) -> JoinHandle<()> { + task::spawn(async move { + info!("Running the node from its spawned task."); + let _node_run_result = spawn_node_child_task(node_config_path). + await. // awaits the completion of spawn_node_child_task. + wait(). // runs the node until completion -- should be running indefinitely. + await; // awaits the completion of the node. + panic!("Node stopped unexpectedly."); + }) +} + +/// Reads the latest block number from the storage. +fn get_latest_block_number(storage_reader: &StorageReader) -> BlockNumber { + let txn = storage_reader.begin_ro_txn().unwrap(); + txn.get_state_marker() + .expect("There should always be a state marker") + .prev() + .expect("There should be a previous block in the storage, set by the test setup") +} + +/// Reads an account nonce after a block number from storage. +fn get_account_nonce( + storage_reader: &StorageReader, + block_number: BlockNumber, + contract_address: ContractAddress, +) -> Nonce { + let txn = storage_reader.begin_ro_txn().unwrap(); + let state_number = StateNumber::unchecked_right_after_block(block_number); + get_nonce_at(&txn, state_number, None, contract_address) + .expect("Should always be Ok(Some(Nonce))") + .expect("Should always be Some(Nonce)") +} + +/// Sample a storage until sufficiently many blocks have been stored. Returns an error if after +/// the given number of attempts the target block number has not been reached. +async fn await_block( + interval_duration: Duration, + target_block_number: BlockNumber, + max_attempts: usize, + storage_reader: &StorageReader, +) -> Result<(), ()> { + let mut interval = interval(interval_duration); + let mut count = 0; + loop { + // Read the latest block number. + let latest_block_number = get_latest_block_number(storage_reader); + count += 1; + + // Check if reached the target block number. + if latest_block_number >= target_block_number { + info!("Found block {} after {} queries.", target_block_number, count); + return Ok(()); + } + + // Check if reached the maximum attempts. + if count > max_attempts { + error!( + "Latest block is {}, expected {}, stopping sampling.", + latest_block_number, target_block_number + ); + return Err(()); + } + + // Wait for the next interval. + interval.tick().await; + } +} + +#[tokio::main] +/// Main entry point of the committer CLI. +async fn main() { + + + let mut tx_generator : MultiAccountTransactionGenerator = create_integration_test_tx_generator(); + + + const EXPECTED_BLOCK_NUMBER: BlockNumber = BlockNumber(15); + + configure_tracing(); + info!("Running integration test setup."); + + // Creating the storage for the test. + + let integration_test_setup = IntegrationTestSetup::new_from_tx_generator(&tx_generator).await; + info!("Integration test setup completed."); + + info!("Running sequencer node."); + let node_run_handle = spawn_run_node(integration_test_setup.node_config_path).await; + + // Wait for the node to start. + match integration_test_setup.is_alive_test_client.await_alive(Duration::from_secs(5), 30).await + { + Ok(_) => {} + Err(_) => panic!("Node is not alive."), + } + + info!("Running integration test simulator."); + + let send_rpc_tx_fn = + &mut |rpc_tx| integration_test_setup.add_tx_http_client.assert_add_tx_success(rpc_tx); + + const ACCOUNT_ID_0: AccountId = 0; + let n_txs = 50; + let sender_address = tx_generator.account_with_id(ACCOUNT_ID_0).sender_address(); + info!("Sending {n_txs} txs."); + let tx_hashes = send_account_txs(tx_generator, ACCOUNT_ID_0, n_txs, send_rpc_tx_fn).await; + + info!("Awaiting until {EXPECTED_BLOCK_NUMBER} blocks have been created."); + + let (batcher_storage_reader, _) = + papyrus_storage::open_storage(integration_test_setup.batcher_storage_config) + .expect("Failed to open batcher's storage"); + + match await_block(Duration::from_secs(5), EXPECTED_BLOCK_NUMBER, 15, &batcher_storage_reader) + .await + { + Ok(_) => {} + Err(_) => panic!("Did not reach expected block number."), + } + + info!("Shutting the node down."); + node_run_handle.abort(); + let res = node_run_handle.await; + assert!( + res.expect_err("Node should have been stopped.").is_cancelled(), + "Node should have been stopped." + ); + + info!("Verifying tx sender account nonce."); + let expected_nonce_value = tx_hashes.len() + 1; + let expected_nonce = + Nonce(Felt::from_hex_unchecked(format!("0x{:X}", expected_nonce_value).as_str())); + let nonce = get_account_nonce(&batcher_storage_reader, EXPECTED_BLOCK_NUMBER, sender_address); + assert_eq!(nonce, expected_nonce); +} diff --git a/crates/starknet_sequencer_node/src/test_utils/compilation.rs b/crates/starknet_sequencer_node/src/test_utils/compilation.rs index 645fe536ae..8fc3e8ff70 100644 --- a/crates/starknet_sequencer_node/src/test_utils/compilation.rs +++ b/crates/starknet_sequencer_node/src/test_utils/compilation.rs @@ -1,6 +1,7 @@ +use std::io; use std::process::{Command, ExitStatus, Stdio}; -use std::{env, io}; +use infra_utils::path::resolve_project_relative_path; use tracing::info; #[cfg(test)] @@ -17,15 +18,17 @@ pub enum NodeCompilationError { /// Compiles the node using `cargo build` for testing purposes. fn compile_node() -> io::Result { - info!("Compiling the project"); - // Get the current working directory for the project - let project_path = env::current_dir().expect("Failed to get current directory"); + info!("Compiling the starknet_sequencer_node binary"); + let project_path = resolve_project_relative_path(".").expect("Failed to resolve project path"); + info!("project_path {:?}", project_path); // Run `cargo build` to compile the project let compilation_result = Command::new("cargo") .arg("build") + .arg("--bin") + .arg("starknet_sequencer_node") .current_dir(&project_path) - .arg("--quiet") + // .arg("--quiet") .stderr(Stdio::inherit()) .stdout(Stdio::inherit()) .status();