Skip to content

Commit f2610af

Browse files
committed
Node/testing: better error handlings and documentation
1 parent f02338e commit f2610af

File tree

1 file changed

+151
-13
lines changed
  • node/testing/src/scenario

1 file changed

+151
-13
lines changed

node/testing/src/scenario/mod.rs

Lines changed: 151 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,33 @@
1+
//! # Scenario Management
2+
//!
3+
//! This module provides functionality for managing test scenarios, including
4+
//! loading, saving, and executing deterministic test sequences.
5+
//!
6+
//! ## How Scenarios Work
7+
//!
8+
//! ### Storage Format
9+
//! Scenarios are stored as JSON files in the `res/scenarios/` directory relative
10+
//! to the testing crate. Each scenario file contains:
11+
//! - **ScenarioInfo**: Metadata (ID, description, parent relationships, node configs)
12+
//! - **ScenarioSteps**: Ordered sequence of test actions to execute
13+
//!
14+
//! ### Load Process
15+
//! 1. **File Location**: `load()` reads from `{CARGO_MANIFEST_DIR}/res/scenarios/{id}.json`
16+
//! 2. **JSON Parsing**: Deserializes the file into a `Scenario` struct
17+
//! 3. **Error Handling**: Returns `anyhow::Error` if file doesn't exist or is malformed
18+
//!
19+
//! ### Save Process
20+
//! 1. **Atomic Write**: Uses temporary file + rename for atomic operations
21+
//! 2. **Directory Creation**: Automatically creates `res/scenarios/` if needed
22+
//! 3. **JSON Format**: Pretty-prints JSON for human readability
23+
//! 4. **Temporary Files**: `.tmp.{scenario_id}.json` during write, renamed on success
24+
//!
25+
//! ### Scenario Inheritance
26+
//! Scenarios can have parent-child relationships where child scenarios inherit
27+
//! setup steps from their parents, enabling composition and reuse.
28+
//!
29+
//! For usage examples, see the [testing documentation](https://o1-labs.github.io/mina-rust/developers/testing/scenario-tests).
30+
131
mod id;
232
pub use id::ScenarioId;
333

@@ -7,6 +37,8 @@ pub use step::{ListenerNode, ScenarioStep};
737
mod event_details;
838
pub use event_details::event_details;
939

40+
use anyhow::Context;
41+
use mina_core::log::{debug, info, system_time};
1042
use serde::{Deserialize, Serialize};
1143

1244
use crate::node::NodeTestingConfig;
@@ -71,42 +103,148 @@ impl Scenario {
71103
}
72104

73105
pub async fn list() -> Result<Vec<ScenarioInfo>, anyhow::Error> {
74-
let mut files = tokio::fs::read_dir(Self::PATH).await?;
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+
})?;
75114
let mut list = vec![];
76115

77116
while let Some(file) = files.next_entry().await? {
78-
let encoded = tokio::fs::read(file.path()).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+
})?;
79121
// TODO(binier): maybe somehow only parse info part of json?
80-
let full: Self = serde_json::from_slice(&encoded)?;
122+
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+
})?;
81128
list.push(full.info);
82129
}
83130

84131
Ok(list)
85132
}
86133

134+
/// Load a scenario from disk by ID.
135+
///
136+
/// This method reads the scenario file from `res/scenarios/{id}.json`,
137+
/// deserializes it from JSON, and returns the complete scenario including
138+
/// both metadata and steps.
139+
///
140+
/// # Arguments
141+
/// * `id` - The scenario identifier used to construct the file path
142+
///
143+
/// # Returns
144+
/// * `Ok(Scenario)` - Successfully loaded scenario
145+
/// * `Err(anyhow::Error)` - File not found, invalid JSON, or I/O error
146+
/// ```
87147
pub async fn load(id: &ScenarioId) -> Result<Self, anyhow::Error> {
88-
let encoded = tokio::fs::read(Self::file_path_by_id(id)).await?;
89-
Ok(serde_json::from_slice(&encoded)?)
148+
let path = Self::file_path_by_id(id);
149+
debug!(system_time(); "Loading scenario '{}' from file '{}'", id, path);
150+
let encoded = tokio::fs::read(&path).await.with_context(|| {
151+
format!(
152+
"Failed to read scenario file '{}'. Ensure the scenario exists. \
153+
If using scenarios-run, the scenario must be generated first using \
154+
scenarios-generate, or check if the required feature flags (like \
155+
'p2p-webrtc') are enabled",
156+
path
157+
)
158+
})?;
159+
let scenario = serde_json::from_slice(&encoded)
160+
.with_context(|| format!("Failed to parse scenario file '{}' as valid JSON", path))?;
161+
info!(system_time(); "Successfully loaded scenario '{}'", id);
162+
Ok(scenario)
90163
}
91164

165+
/// Reload this scenario from disk, discarding any in-memory changes.
92166
pub async fn reload(&mut self) -> Result<(), anyhow::Error> {
93167
*self = Self::load(&self.info.id).await?;
94168
Ok(())
95169
}
96170

171+
/// Save the scenario to disk using atomic write operations.
172+
///
173+
/// This method implements atomic writes by:
174+
/// 1. Creating the scenarios directory if it doesn't exist
175+
/// 2. Writing to a temporary file (`.tmp.{id}.json`)
176+
/// 3. Pretty-printing JSON for human readability
177+
/// 4. Atomically renaming the temp file to the final name
178+
///
179+
/// This ensures the scenario file is never in a partially-written state,
180+
/// preventing corruption during concurrent access or system crashes.
181+
///
182+
/// # File Location
183+
/// Saves to: `{CARGO_MANIFEST_DIR}/res/scenarios/{id}.json`
184+
///
185+
/// # Errors
186+
/// Returns error if:
187+
/// - Cannot create the scenarios directory
188+
/// - Cannot serialize scenario to JSON
189+
/// - File I/O operations fail
190+
/// - Atomic rename fails
97191
pub async fn save(&self) -> Result<(), anyhow::Error> {
98192
let tmp_file = self.tmp_file_path();
99-
let encoded = serde_json::to_vec_pretty(self)?;
100-
tokio::fs::create_dir_all(Self::PATH).await?;
101-
tokio::fs::write(&tmp_file, encoded).await?;
102-
Ok(tokio::fs::rename(tmp_file, self.file_path()).await?)
193+
let final_file = self.file_path();
194+
195+
debug!(system_time(); "Saving scenario '{}' to file '{}'", self.info.id, final_file);
196+
197+
let encoded = serde_json::to_vec_pretty(self)
198+
.with_context(|| format!("Failed to serialize scenario '{}' to JSON", self.info.id))?;
199+
200+
tokio::fs::create_dir_all(Self::PATH)
201+
.await
202+
.with_context(|| format!("Failed to create scenarios directory '{}'", Self::PATH))?;
203+
204+
tokio::fs::write(&tmp_file, encoded)
205+
.await
206+
.with_context(|| format!("Failed to write temporary scenario file '{}'", tmp_file))?;
207+
208+
tokio::fs::rename(&tmp_file, &final_file)
209+
.await
210+
.with_context(|| {
211+
format!(
212+
"Failed to rename temporary file '{}' to final scenario file '{}'",
213+
tmp_file, final_file
214+
)
215+
})?;
216+
217+
info!(system_time(); "Successfully saved scenario '{}'", self.info.id);
218+
Ok(())
103219
}
104220

221+
/// Synchronous version of `save()` for use in non-async contexts.
222+
///
223+
/// Implements the same atomic write pattern as `save()` but uses
224+
/// blocking I/O operations instead of async.
105225
pub fn save_sync(&self) -> Result<(), anyhow::Error> {
106226
let tmp_file = self.tmp_file_path();
107-
let encoded = serde_json::to_vec_pretty(self)?;
108-
std::fs::create_dir_all(Self::PATH)?;
109-
std::fs::write(&tmp_file, encoded)?;
110-
Ok(std::fs::rename(tmp_file, self.file_path())?)
227+
let final_file = self.file_path();
228+
229+
debug!(system_time(); "Saving scenario '{}' to file '{}'", self.info.id, final_file);
230+
231+
let encoded = serde_json::to_vec_pretty(self)
232+
.with_context(|| format!("Failed to serialize scenario '{}' to JSON", self.info.id))?;
233+
234+
std::fs::create_dir_all(Self::PATH)
235+
.with_context(|| format!("Failed to create scenarios directory '{}'", Self::PATH))?;
236+
237+
std::fs::write(&tmp_file, encoded)
238+
.with_context(|| format!("Failed to write temporary scenario file '{}'", tmp_file))?;
239+
240+
std::fs::rename(&tmp_file, &final_file).with_context(|| {
241+
format!(
242+
"Failed to rename temporary file '{}' to final scenario file '{}'",
243+
tmp_file, final_file
244+
)
245+
})?;
246+
247+
info!(system_time(); "Successfully saved scenario '{}'", self.info.id);
248+
Ok(())
111249
}
112250
}

0 commit comments

Comments
 (0)