crazy_train/
runner.rs

1//! This module defines the [`Runner`] struct, which is responsible for managing and executing a sequence of steps in a defined order.
2//! Each step can be customized with initial commands, checks, and tests, providing a flexible and extensible execution flow.
3//!
4//! The `Runner` can also generate and display an execution plan for the steps to be taken.
5//! The steps can be randomized using the [`Randomizer`], enhancing the unpredictability of the execution.
6//!
7use crate::{
8    executer,
9    randomizer::Randomizer,
10    step::{self, StepTrait},
11    Error, Result,
12};
13use colored::Colorize;
14use std::time::Instant;
15
16/// A struct that orchestrates the execution of a series of steps.
17pub struct Runner {
18    steps: Vec<Box<dyn StepTrait>>,
19    init: Option<Box<dyn StepTrait>>,
20    randomizer: Randomizer,
21}
22
23/// Creates a new [`Runner`] instance with the given steps.
24#[must_use]
25pub fn new(steps: Vec<Box<dyn StepTrait>>) -> Runner {
26    Runner {
27        steps,
28        init: None,
29        randomizer: Randomizer::default(),
30    }
31}
32
33impl Runner {
34    /// Sets an initial step for the runner.
35    #[must_use]
36    pub fn init_step(mut self, step: Box<dyn StepTrait>) -> Self {
37        self.init = Some(step);
38        self
39    }
40
41    /// Sets a custom randomizer for the runner.
42    #[must_use]
43    pub fn randomizer(mut self, randomizer: Randomizer) -> Self {
44        self.randomizer = randomizer;
45        self
46    }
47
48    // Dumps the execution plan for the steps to be executed.
49    ///
50    /// # Errors
51    ///
52    /// when could not present the plan
53    pub fn dump_plan(&self) -> Result<String> {
54        let mut output: Vec<String> = Vec::new();
55
56        output.push("====================================".to_string());
57        output.push("          Execution Plan Dump        ".green().to_string());
58        output.push("====================================".to_string());
59        output.push(format!("{}: {}", "Step Count".bold(), &self.steps.len()));
60        output.push(format!("{}: {}", "Seed".bold(), &self.randomizer.seed));
61        output.push("------------------------------------".to_string());
62
63        for (i, step) in self.steps.iter().enumerate() {
64            let execution_plan = step.plan(&self.randomizer)?;
65            output.push(
66                format!("Step {}: {}", i + 1, execution_plan.id)
67                    .green()
68                    .to_string(),
69            );
70            output.push("------------------------------------".to_string());
71            output.push("Command:".bold().to_string());
72            output.push(execution_plan.command.clone());
73            output.push("State:".bold().to_string());
74            output.push("---".to_string());
75
76            let state = serde_yaml::to_string(&step.to_yaml()).unwrap_or_default();
77            output.push(state);
78
79            output.push("------------------------------------".to_string());
80        }
81
82        Ok(output.join("\n"))
83    }
84
85    /// Executes the steps in the runner.
86    ///
87    /// # Errors
88    /// On the first step that fails
89    pub fn run(&self) -> Result<()> {
90        println!("{}", self.dump_plan()?);
91        for step in &self.steps {
92            let step_plan = step.plan(&self.randomizer)?;
93
94            println!();
95            println!("{}", format!("Run step: {}", step_plan.id).yellow());
96            println!();
97
98            step.setup()?;
99            let start = Instant::now();
100            println!("{}", "Execute plan...".yellow());
101            let result = step.plan(&self.randomizer)?.execute()?;
102            println!(
103                "{}",
104                format!("Execute plan finished in {:?}", start.elapsed()).yellow()
105            );
106            let is_success =
107                step.is_success(&result, &step_plan.ctx)
108                    .map_err(|err| Error::StepError {
109                        kind: step::Kind::Plan,
110                        description: err.to_string(),
111                        command_output: result,
112                    })?;
113
114            if !is_success {
115                continue;
116            }
117
118            if let Some(check_command) = step.run_check() {
119                let start = Instant::now();
120                println!("{}", "Execute check...".yellow());
121                let output = executer::run_sh(&check_command)?;
122                println!(
123                    "{}",
124                    format!("Execute check finished in {:?}", start.elapsed()).yellow()
125                );
126                if output.status_code != Some(0) {
127                    return Err(Error::StepError {
128                        kind: step::Kind::Check,
129                        description: "check not finish with status code 0".to_string(),
130                        command_output: output,
131                    });
132                }
133            }
134
135            if let Some(test_command) = step.run_test() {
136                let start = Instant::now();
137                println!("{}", "Execute test...".yellow());
138                let output = executer::run_sh(&test_command)?;
139                println!(
140                    "{}",
141                    format!("Execute tests finished in {:?}", start.elapsed()).yellow()
142                );
143                if output.status_code != Some(0) {
144                    return Err(Error::StepError {
145                        kind: step::Kind::Test,
146                        description: "test command not finish with status code 0".to_string(),
147                        command_output: output,
148                    });
149                }
150            }
151        }
152
153        println!("{}", "Execution plan is pass successfully".green());
154        Ok(())
155    }
156}
157
158#[cfg(test)]
159mod tests {
160
161    use std::{collections::HashMap, path::PathBuf};
162
163    use serde::{Deserialize, Serialize};
164    use step::PlanCtx;
165
166    use super::*;
167    use crate::{executer::Output, generator::StringDef, step::Plan};
168
169    #[derive(Serialize, Deserialize)]
170    struct TestStepOne {
171        location: PathBuf,
172    }
173
174    #[derive(Serialize, Deserialize)]
175    struct TestStepTwo {
176        location: PathBuf,
177    }
178
179    impl StepTrait for TestStepOne {
180        fn setup(&self) -> crate::errors::Result<()> {
181            Ok(std::fs::create_dir_all(&self.location)?)
182        }
183
184        fn plan(&self, randomizer: &Randomizer) -> Result<Plan> {
185            let eco_string = randomizer.string(StringDef::default()).to_string();
186            Ok(Plan::with_vars::<Self>(
187                format!(
188                    "echo {eco_string} >> {}",
189                    self.location.join("test.txt").display()
190                ),
191                HashMap::from([("foo".to_string(), "bar".to_string())]),
192            ))
193        }
194
195        fn is_success(
196            &self,
197            execution_result: &Output,
198            plan_ctx: &PlanCtx,
199        ) -> Result<bool, &'static str> {
200            if let Some(foo_var) = plan_ctx.vars.get("foo") {
201                if foo_var != "bar" {
202                    return Err("foo value should be equal to var");
203                }
204            } else {
205                return Err("foo plan ctx var not found");
206            };
207
208            if execution_result.status_code == Some(0) {
209                Ok(true)
210            } else {
211                Err("status code should be 0")
212            }
213        }
214
215        fn run_check(&self) -> Option<String> {
216            Some(format!(
217                "test -f {}",
218                self.location.join("test.txt").display()
219            ))
220        }
221
222        fn run_test(&self) -> Option<String> {
223            Some(format!(
224                "test -f {}",
225                self.location.join("test.txt").display()
226            ))
227        }
228
229        fn to_yaml(&self) -> serde_yaml::Value {
230            serde_yaml::to_value(self).expect("serialize")
231        }
232    }
233
234    impl StepTrait for TestStepTwo {
235        fn setup(&self) -> crate::errors::Result<()> {
236            Ok(std::fs::create_dir_all(&self.location)?)
237        }
238
239        fn plan(&self, randomizer: &Randomizer) -> Result<Plan> {
240            let eco_string = randomizer.string(StringDef::default()).to_string();
241            Ok(Plan::with_vars::<Self>(
242                format!(
243                    "cat {eco_string} >> {}",
244                    self.location.join("test.txt").display()
245                ),
246                HashMap::from([("foo".to_string(), "bar".to_string())]),
247            ))
248        }
249
250        fn is_success(
251            &self,
252            execution_result: &Output,
253            _plan_ctx: &PlanCtx,
254        ) -> Result<bool, &'static str> {
255            if execution_result.status_code == Some(1) {
256                Ok(true)
257            } else {
258                Err("status code should be 1")
259            }
260        }
261
262        fn to_yaml(&self) -> serde_yaml::Value {
263            serde_yaml::to_value(self).expect("serialize")
264        }
265    }
266
267    #[test]
268    fn can_run() {
269        let base_location = std::env::temp_dir().join("crazy-train");
270        let location_step_1 = base_location.join("step-1");
271        let location_step_2 = base_location.join("step-2");
272
273        let step_one = TestStepOne {
274            location: location_step_1,
275        };
276        let step_two = TestStepTwo {
277            location: location_step_2,
278        };
279        let randomaizer = Randomizer::with_seed(42);
280        let runner = new(vec![Box::new(step_one), Box::new(step_two)]).randomizer(randomaizer);
281
282        assert!(runner.run().is_ok());
283    }
284}