mina_node_testing/
main.rs

1//! # Mina Node Testing CLI
2//!
3//! Command-line interface for running Mina node scenario tests.
4//! Provides tools for generating, running, and managing deterministic
5//! blockchain testing scenarios.
6//!
7//! ## Documentation
8//!
9//! For detailed documentation and usage examples, see:
10//! - [Scenario Tests](https://o1-labs.github.io/mina-rust/developers/testing/scenario-tests) - Complete testing guide
11//! - [Testing Framework](https://o1-labs.github.io/mina-rust/developers/testing/testing-framework) - Testing architecture
12//!
13//! ## Quick Start
14//!
15//! List all available scenarios:
16//! ```bash
17//! cargo run --release --bin mina-node-testing -- scenarios-list
18//! ```
19//!
20//! Run a specific scenario:
21//! ```bash
22//! cargo run --release --bin mina-node-testing -- scenarios-run --name p2p-signaling
23//! ```
24
25use clap::Parser;
26
27use mina_node_testing::{
28    cluster::{Cluster, ClusterConfig},
29    exit_with_error,
30    scenario::Scenario,
31    scenarios::Scenarios,
32    server, setup,
33};
34use node::p2p::webrtc::Host;
35
36pub type CommandError = anyhow::Error;
37
38#[derive(Debug, clap::Parser)]
39#[command(name = "mina-testing", about = "Mina Testing Cli")]
40pub struct MinaTestingCli {
41    #[command(subcommand)]
42    pub command: Command,
43}
44
45#[derive(Debug, clap::Subcommand)]
46pub enum Command {
47    Server(CommandServer),
48
49    ScenariosGenerate(CommandScenariosGenerate),
50    ScenariosRun(CommandScenariosRun),
51    ScenariosList(CommandScenariosList),
52}
53
54#[derive(Debug, clap::Args)]
55pub struct CommandServer {
56    #[arg(long, short, default_value = "127.0.0.1")]
57    pub host: Host,
58
59    #[arg(long, short, default_value = "11000")]
60    pub port: u16,
61    #[arg(long, short)]
62    pub ssl_port: Option<u16>,
63}
64
65#[derive(Debug, clap::Args)]
66pub struct CommandScenariosGenerate {
67    #[arg(long, short)]
68    pub name: Option<String>,
69    #[arg(long, short)]
70    pub use_debugger: bool,
71    #[arg(long, short)]
72    pub webrtc: bool,
73    #[arg(long, short = 'o', default_value = "stdout", value_enum)]
74    pub output: OutputFormat,
75}
76
77#[derive(Debug, Clone, clap::ValueEnum)]
78pub enum OutputFormat {
79    Stdout,
80    Json,
81}
82
83/// Run scenario located at `res/scenarios`.
84#[derive(Debug, clap::Args)]
85pub struct CommandScenariosRun {
86    /// Name of the scenario.
87    ///
88    /// Must match filename in `res/scenarios` (without an extension).
89    #[arg(long, short)]
90    pub name: String,
91}
92
93#[derive(Debug, clap::Args)]
94pub struct CommandScenariosList {}
95
96impl Command {
97    pub fn run(self) -> Result<(), crate::CommandError> {
98        let rt = setup();
99        let _rt_guard = rt.enter();
100
101        let (shutdown_tx, shutdown_rx) = mina_core::channels::oneshot::channel();
102        let mut shutdown_tx = Some(shutdown_tx);
103
104        ctrlc::set_handler(move || match shutdown_tx.take() {
105            Some(tx) => {
106                let _ = tx.send(());
107            }
108            None => {
109                std::process::exit(1);
110            }
111        })
112        .expect("Error setting Ctrl-C handler");
113
114        match self {
115            Self::Server(args) => {
116                server(rt, args.host, args.port, args.ssl_port);
117                Ok(())
118            }
119            Self::ScenariosGenerate(cmd) => {
120                #[cfg(feature = "scenario-generators")]
121                {
122                    let output_format = cmd.output.clone();
123                    let run_scenario = |scenario: Scenarios| -> Result<_, anyhow::Error> {
124                        let mut config = scenario.default_cluster_config()?;
125                        if cmd.use_debugger {
126                            config.use_debugger();
127                        }
128                        if cmd.webrtc {
129                            config.set_all_rust_to_rust_use_webrtc();
130                        }
131                        Ok((scenario, config))
132                    };
133                    let fut = async move {
134                        if let Some(name) = cmd.name {
135                            if let Some(scenario) = Scenarios::find_by_name(&name) {
136                                let (scenario, config) = run_scenario(scenario)?;
137                                match output_format {
138                                    OutputFormat::Json => {
139                                        scenario.run_and_save_from_scratch(config).await
140                                    }
141                                    OutputFormat::Stdout => {
142                                        scenario.run_only_from_scratch(config).await
143                                    }
144                                }
145                            } else {
146                                anyhow::bail!("no such scenario: \"{name}\"");
147                            }
148                        } else {
149                            for scenario in Scenarios::iter() {
150                                let (scenario, config) = run_scenario(scenario)?;
151                                match output_format {
152                                    OutputFormat::Json => {
153                                        scenario.run_and_save_from_scratch(config).await
154                                    }
155                                    OutputFormat::Stdout => {
156                                        scenario.run_only_from_scratch(config).await
157                                    }
158                                }
159                            }
160                        }
161                        Ok(())
162                    };
163                    rt.block_on(async {
164                        tokio::select! {
165                            res = fut => res,
166                            _ = shutdown_rx => {
167                                anyhow::bail!("Received ctrl-c signal! shutting down...");
168                            }
169                        }
170                    })
171                }
172                #[cfg(not(feature = "scenario-generators"))]
173                Err("binary not compiled with `scenario-generators` feature"
174                    .to_owned()
175                    .into())
176            }
177            Self::ScenariosRun(cmd) => {
178                let mut config = ClusterConfig::new(None).map_err(|err| {
179                    anyhow::anyhow!("failed to create cluster configuration: {err}")
180                })?;
181                config.set_replay();
182
183                let id = cmd.name.parse()?;
184                let fut = async move {
185                    let mut cluster = Cluster::new(config);
186                    cluster.start(Scenario::load(&id).await?).await?;
187                    cluster.exec_to_end().await?;
188                    for (node_id, node) in cluster.nodes_iter() {
189                        let Some(best_tip) = node.state().transition_frontier.best_tip() else {
190                            continue;
191                        };
192
193                        eprintln!(
194                            "[node_status] node_{node_id} {} - {} [{}]",
195                            best_tip.height(),
196                            best_tip.hash(),
197                            best_tip.producer()
198                        );
199                    }
200                    Ok(())
201                };
202                rt.block_on(async {
203                    tokio::select! {
204                        res = fut => res,
205                        _ = shutdown_rx => {
206                            anyhow::bail!("Received ctrl-c signal! shutting down...");
207                        }
208                    }
209                })
210            }
211            Self::ScenariosList(_) => {
212                println!("Available scenarios:");
213                for scenario in Scenarios::iter() {
214                    println!("  {}", scenario.to_str())
215                }
216                Ok(())
217            }
218        }
219    }
220}
221
222pub fn main() {
223    match MinaTestingCli::parse().command.run() {
224        Ok(_) => {}
225        Err(err) => exit_with_error(err),
226    }
227}