voirs_cli/workflow/
state.rs1use super::executor::StepResult;
6use crate::error::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13pub enum ExecutionState {
14 Pending,
16 Running,
18 Completed,
20 Failed,
22 Stopped,
24 Paused,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct WorkflowState {
31 pub workflow_name: String,
33 pub state: ExecutionState,
35 pub variables: HashMap<String, serde_json::Value>,
37 pub completed_steps: HashMap<String, StepResult>,
39 pub skipped_steps: Vec<String>,
41 pub current_step: Option<String>,
43 pub total_retries: usize,
45 pub last_updated: chrono::DateTime<chrono::Utc>,
47}
48
49impl WorkflowState {
50 pub fn new(workflow_name: String) -> Self {
52 Self {
53 workflow_name,
54 state: ExecutionState::Pending,
55 variables: HashMap::new(),
56 completed_steps: HashMap::new(),
57 skipped_steps: Vec::new(),
58 current_step: None,
59 total_retries: 0,
60 last_updated: chrono::Utc::now(),
61 }
62 }
63
64 pub fn mark_running(&mut self) {
66 self.state = ExecutionState::Running;
67 self.last_updated = chrono::Utc::now();
68 }
69
70 pub fn mark_completed(&mut self) {
72 self.state = ExecutionState::Completed;
73 self.last_updated = chrono::Utc::now();
74 }
75
76 pub fn mark_failed(&mut self) {
78 self.state = ExecutionState::Failed;
79 self.last_updated = chrono::Utc::now();
80 }
81
82 pub fn set_current_step(&mut self, step_name: Option<String>) {
84 self.current_step = step_name;
85 self.last_updated = chrono::Utc::now();
86 }
87
88 pub fn can_resume(&self) -> bool {
90 matches!(
91 self.state,
92 ExecutionState::Failed | ExecutionState::Stopped | ExecutionState::Paused
93 )
94 }
95}
96
97pub struct StateManager {
99 storage_dir: PathBuf,
100}
101
102impl StateManager {
103 pub fn new(storage_dir: PathBuf) -> Self {
105 Self { storage_dir }
106 }
107
108 pub async fn save(&self, workflow_name: &str, state: &WorkflowState) -> Result<()> {
110 tokio::fs::create_dir_all(&self.storage_dir).await?;
111
112 let filename = format!("{}.state.json", workflow_name);
113 let path = self.storage_dir.join(filename);
114
115 let json = serde_json::to_string_pretty(state)?;
116 tokio::fs::write(path, json).await?;
117
118 Ok(())
119 }
120
121 pub async fn load(&self, workflow_name: &str) -> Result<WorkflowState> {
123 let filename = format!("{}.state.json", workflow_name);
124 let path = self.storage_dir.join(filename);
125
126 let json = tokio::fs::read_to_string(path).await?;
127 let state = serde_json::from_str(&json)?;
128
129 Ok(state)
130 }
131
132 pub async fn delete(&self, workflow_name: &str) -> Result<()> {
134 let filename = format!("{}.state.json", workflow_name);
135 let path = self.storage_dir.join(filename);
136
137 if path.exists() {
138 tokio::fs::remove_file(path).await?;
139 }
140
141 Ok(())
142 }
143
144 pub async fn list_states(&self) -> Result<Vec<String>> {
146 let mut states = Vec::new();
147
148 if !self.storage_dir.exists() {
149 return Ok(states);
150 }
151
152 let mut entries = tokio::fs::read_dir(&self.storage_dir).await?;
153
154 while let Some(entry) = entries.next_entry().await? {
155 let path = entry.path();
156 if let Some(filename) = path.file_name() {
157 if let Some(name_str) = filename.to_str() {
158 if name_str.ends_with(".state.json") {
159 let workflow_name = name_str.strip_suffix(".state.json").unwrap();
160 states.push(workflow_name.to_string());
161 }
162 }
163 }
164 }
165
166 Ok(states)
167 }
168
169 pub async fn exists(&self, workflow_name: &str) -> bool {
171 let filename = format!("{}.state.json", workflow_name);
172 let path = self.storage_dir.join(filename);
173 path.exists()
174 }
175
176 pub async fn get_info(&self, workflow_name: &str) -> Result<StateInfo> {
178 let state = self.load(workflow_name).await?;
179
180 Ok(StateInfo {
181 workflow_name: state.workflow_name,
182 state: state.state,
183 completed_steps: state.completed_steps.len(),
184 total_steps: state.completed_steps.len() + state.skipped_steps.len(),
185 current_step: state.current_step,
186 last_updated: state.last_updated,
187 })
188 }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct StateInfo {
194 pub workflow_name: String,
196 pub state: ExecutionState,
198 pub completed_steps: usize,
200 pub total_steps: usize,
202 pub current_step: Option<String>,
204 pub last_updated: chrono::DateTime<chrono::Utc>,
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use std::env;
212
213 #[test]
214 fn test_workflow_state_creation() {
215 let state = WorkflowState::new("test-workflow".to_string());
216 assert_eq!(state.workflow_name, "test-workflow");
217 assert_eq!(state.state, ExecutionState::Pending);
218 assert_eq!(state.completed_steps.len(), 0);
219 }
220
221 #[test]
222 fn test_workflow_state_transitions() {
223 let mut state = WorkflowState::new("test".to_string());
224
225 assert_eq!(state.state, ExecutionState::Pending);
226
227 state.mark_running();
228 assert_eq!(state.state, ExecutionState::Running);
229
230 state.mark_completed();
231 assert_eq!(state.state, ExecutionState::Completed);
232 }
233
234 #[test]
235 fn test_workflow_state_can_resume() {
236 let mut state = WorkflowState::new("test".to_string());
237
238 state.mark_failed();
239 assert!(state.can_resume());
240
241 state.state = ExecutionState::Stopped;
242 assert!(state.can_resume());
243
244 state.state = ExecutionState::Paused;
245 assert!(state.can_resume());
246
247 state.mark_completed();
248 assert!(!state.can_resume());
249 }
250
251 #[tokio::test]
252 async fn test_state_manager_creation() {
253 let temp_dir = env::temp_dir().join("voirs_state_test");
254 let manager = StateManager::new(temp_dir);
255 assert!(true);
257 }
258
259 #[tokio::test]
260 async fn test_state_manager_save_and_load() {
261 let temp_dir = env::temp_dir().join("voirs_state_test_2");
262 let manager = StateManager::new(temp_dir);
263
264 let mut state = WorkflowState::new("test-workflow".to_string());
265 state.mark_running();
266 state
267 .variables
268 .insert("key1".to_string(), serde_json::json!("value1"));
269
270 manager.save("test-workflow", &state).await.unwrap();
271
272 let loaded_state = manager.load("test-workflow").await.unwrap();
273 assert_eq!(loaded_state.workflow_name, "test-workflow");
274 assert_eq!(loaded_state.state, ExecutionState::Running);
275 assert_eq!(loaded_state.variables.len(), 1);
276 }
277
278 #[tokio::test]
279 async fn test_state_manager_exists() {
280 let temp_dir = env::temp_dir().join("voirs_state_test_3");
281 let manager = StateManager::new(temp_dir);
282
283 assert!(!manager.exists("nonexistent").await);
284
285 let state = WorkflowState::new("exists-test".to_string());
286 manager.save("exists-test", &state).await.unwrap();
287
288 assert!(manager.exists("exists-test").await);
289 }
290
291 #[tokio::test]
292 async fn test_state_manager_delete() {
293 let temp_dir = env::temp_dir().join("voirs_state_test_4");
294 let manager = StateManager::new(temp_dir);
295
296 let state = WorkflowState::new("delete-test".to_string());
297 manager.save("delete-test", &state).await.unwrap();
298
299 assert!(manager.exists("delete-test").await);
300
301 manager.delete("delete-test").await.unwrap();
302
303 assert!(!manager.exists("delete-test").await);
304 }
305
306 #[tokio::test]
307 async fn test_state_manager_list_states() {
308 let temp_dir = env::temp_dir().join("voirs_state_test_5");
309 let manager = StateManager::new(temp_dir);
310
311 let state1 = WorkflowState::new("workflow1".to_string());
312 let state2 = WorkflowState::new("workflow2".to_string());
313
314 manager.save("workflow1", &state1).await.unwrap();
315 manager.save("workflow2", &state2).await.unwrap();
316
317 let states = manager.list_states().await.unwrap();
318 assert!(states.len() >= 2);
319 assert!(
320 states.contains(&"workflow1".to_string()) || states.contains(&"workflow2".to_string())
321 );
322 }
323}