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+ 
131mod  id; 
232pub  use  id:: ScenarioId ; 
333
@@ -7,6 +37,8 @@ pub use step::{ListenerNode, ScenarioStep};
737mod  event_details; 
838pub  use  event_details:: event_details; 
939
40+ use  anyhow:: Context ; 
41+ use  mina_core:: log:: { debug,  info,  system_time} ; 
1042use  serde:: { Deserialize ,  Serialize } ; 
1143
1244use  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