mina_node_testing/scenario/
mod.rs1mod id;
32pub use id::ScenarioId;
33
34mod step;
35pub use step::{ListenerNode, ScenarioStep};
36
37mod event_details;
38pub use event_details::event_details;
39
40use anyhow::Context;
41use mina_core::log::{debug, info, system_time};
42use serde::{Deserialize, Serialize};
43
44use crate::node::NodeTestingConfig;
45
46#[derive(Serialize, Deserialize, Debug, Clone)]
47pub struct Scenario {
48 pub info: ScenarioInfo,
49 pub steps: Vec<ScenarioStep>,
50}
51
52#[derive(Serialize, Deserialize, Debug, Clone)]
53pub struct ScenarioInfo {
54 pub id: ScenarioId,
55 pub description: String,
56 pub parent_id: Option<ScenarioId>,
57 pub nodes: Vec<NodeTestingConfig>,
59}
60
61impl Scenario {
62 pub const PATH: &'static str = concat!(env!("CARGO_MANIFEST_DIR"), "/res/scenarios");
63
64 pub fn new(id: ScenarioId, parent_id: Option<ScenarioId>) -> Self {
65 Self {
66 info: ScenarioInfo {
67 id,
68 description: String::new(),
69 parent_id,
70 nodes: vec![],
71 },
72 steps: vec![],
73 }
74 }
75
76 pub fn set_description(&mut self, description: String) {
77 self.info.description = description;
78 }
79
80 pub fn add_node(&mut self, config: NodeTestingConfig) {
81 self.info.nodes.push(config);
82 }
83
84 pub fn add_step(&mut self, step: ScenarioStep) -> Result<(), anyhow::Error> {
85 self.steps.push(step);
86 Ok(())
87 }
88
89 fn tmp_file_path(&self) -> String {
90 format!("{}/.tmp.{}.json", Self::PATH, self.info.id)
91 }
92
93 pub fn file_path(&self) -> String {
94 Self::file_path_by_id(&self.info.id)
95 }
96
97 fn file_path_by_id(id: &ScenarioId) -> String {
98 format!("{}/{}.json", Self::PATH, id)
99 }
100
101 pub fn exists(id: &ScenarioId) -> bool {
102 std::path::Path::new(&Self::file_path_by_id(id)).exists()
103 }
104
105 pub async fn list() -> Result<Vec<ScenarioInfo>, anyhow::Error> {
106 let mut files = tokio::fs::read_dir(Self::PATH).await.with_context(|| {
107 format!(
108 "Failed to read scenarios directory '{}'. Ensure the directory \
109 exists or create it with: mkdir -p {}",
110 Self::PATH,
111 Self::PATH
112 )
113 })?;
114 let mut list = vec![];
115
116 while let Some(file) = files.next_entry().await? {
117 let file_path = file.path();
118 let encoded = tokio::fs::read(&file_path).await.with_context(|| {
119 format!("Failed to read scenario file '{}'", file_path.display())
120 })?;
121 let full: Self = serde_json::from_slice(&encoded).with_context(|| {
123 format!(
124 "Failed to parse scenario file '{}' as valid JSON",
125 file_path.display()
126 )
127 })?;
128 list.push(full.info);
129 }
130
131 Ok(list)
132 }
133
134 pub async fn load(id: &ScenarioId) -> Result<Self, anyhow::Error> {
147 let path = Self::file_path_by_id(id);
148 debug!(system_time(); "Loading scenario '{}' from file '{}'", id, path);
149 let encoded = tokio::fs::read(&path).await.with_context(|| {
150 format!(
151 "Failed to read scenario file '{}'. Ensure the scenario exists. \
152 If using scenarios-run, the scenario must be generated first using \
153 scenarios-generate, or check if the required feature flags (like \
154 'p2p-webrtc') are enabled",
155 path
156 )
157 })?;
158 let scenario = serde_json::from_slice(&encoded)
159 .with_context(|| format!("Failed to parse scenario file '{}' as valid JSON", path))?;
160 info!(system_time(); "Successfully loaded scenario '{}'", id);
161 Ok(scenario)
162 }
163
164 pub async fn reload(&mut self) -> Result<(), anyhow::Error> {
166 *self = Self::load(&self.info.id).await?;
167 Ok(())
168 }
169
170 pub async fn save(&self) -> Result<(), anyhow::Error> {
191 let tmp_file = self.tmp_file_path();
192 let final_file = self.file_path();
193
194 debug!(system_time(); "Saving scenario '{}' to file '{}'", self.info.id, final_file);
195
196 let encoded = serde_json::to_vec_pretty(self)
197 .with_context(|| format!("Failed to serialize scenario '{}' to JSON", self.info.id))?;
198
199 tokio::fs::create_dir_all(Self::PATH)
200 .await
201 .with_context(|| format!("Failed to create scenarios directory '{}'", Self::PATH))?;
202
203 tokio::fs::write(&tmp_file, encoded)
204 .await
205 .with_context(|| format!("Failed to write temporary scenario file '{}'", tmp_file))?;
206
207 tokio::fs::rename(&tmp_file, &final_file)
208 .await
209 .with_context(|| {
210 format!(
211 "Failed to rename temporary file '{}' to final scenario file '{}'",
212 tmp_file, final_file
213 )
214 })?;
215
216 info!(system_time(); "Successfully saved scenario '{}'", self.info.id);
217 Ok(())
218 }
219
220 pub fn save_sync(&self) -> Result<(), anyhow::Error> {
225 let tmp_file = self.tmp_file_path();
226 let final_file = self.file_path();
227
228 debug!(system_time(); "Saving scenario '{}' to file '{}'", self.info.id, final_file);
229
230 let encoded = serde_json::to_vec_pretty(self)
231 .with_context(|| format!("Failed to serialize scenario '{}' to JSON", self.info.id))?;
232
233 std::fs::create_dir_all(Self::PATH)
234 .with_context(|| format!("Failed to create scenarios directory '{}'", Self::PATH))?;
235
236 std::fs::write(&tmp_file, encoded)
237 .with_context(|| format!("Failed to write temporary scenario file '{}'", tmp_file))?;
238
239 std::fs::rename(&tmp_file, &final_file).with_context(|| {
240 format!(
241 "Failed to rename temporary file '{}' to final scenario file '{}'",
242 tmp_file, final_file
243 )
244 })?;
245
246 info!(system_time(); "Successfully saved scenario '{}'", self.info.id);
247 Ok(())
248 }
249}