mockforge_bench/
scenarios.rs

1//! Load testing scenario definitions
2
3use serde::{Deserialize, Serialize};
4
5/// Load testing scenarios
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "kebab-case")]
8pub enum LoadScenario {
9    /// Constant load - maintains steady number of VUs
10    Constant,
11    /// Ramp-up - gradually increases load
12    RampUp,
13    /// Spike - sudden increase in load
14    Spike,
15    /// Stress - continuously increasing load to find breaking point
16    Stress,
17    /// Soak - sustained load over extended period
18    Soak,
19}
20
21impl std::str::FromStr for LoadScenario {
22    type Err = String;
23
24    fn from_str(s: &str) -> Result<Self, Self::Err> {
25        match s.to_lowercase().as_str() {
26            "constant" => Ok(Self::Constant),
27            "ramp-up" | "ramp_up" | "rampup" => Ok(Self::RampUp),
28            "spike" => Ok(Self::Spike),
29            "stress" => Ok(Self::Stress),
30            "soak" => Ok(Self::Soak),
31            _ => Err(format!("Unknown scenario: {}", s)),
32        }
33    }
34}
35
36impl LoadScenario {
37    /// Generate k6 stages configuration for this scenario
38    pub fn generate_stages(&self, duration_secs: u64, max_vus: u32) -> Vec<Stage> {
39        match self {
40            Self::Constant => {
41                vec![Stage {
42                    duration: format!("{}s", duration_secs),
43                    target: max_vus,
44                }]
45            }
46            Self::RampUp => {
47                let ramp_duration = duration_secs / 3;
48                let sustain_duration = duration_secs / 3;
49                let ramp_down_duration = duration_secs - ramp_duration - sustain_duration;
50
51                vec![
52                    Stage {
53                        duration: format!("{}s", ramp_duration / 2),
54                        target: max_vus / 4,
55                    },
56                    Stage {
57                        duration: format!("{}s", ramp_duration / 2),
58                        target: max_vus / 2,
59                    },
60                    Stage {
61                        duration: format!("{}s", sustain_duration),
62                        target: max_vus,
63                    },
64                    Stage {
65                        duration: format!("{}s", ramp_down_duration),
66                        target: 0,
67                    },
68                ]
69            }
70            Self::Spike => {
71                let baseline_duration = duration_secs / 5;
72                let spike_duration = duration_secs / 10;
73                let recovery_duration = duration_secs - (baseline_duration * 2) - spike_duration;
74
75                vec![
76                    Stage {
77                        duration: format!("{}s", baseline_duration),
78                        target: max_vus / 10,
79                    },
80                    Stage {
81                        duration: format!("{}s", spike_duration),
82                        target: max_vus,
83                    },
84                    Stage {
85                        duration: format!("{}s", recovery_duration),
86                        target: max_vus / 10,
87                    },
88                    Stage {
89                        duration: format!("{}s", baseline_duration),
90                        target: 0,
91                    },
92                ]
93            }
94            Self::Stress => {
95                let step_duration = duration_secs / 6;
96                let step_vus = max_vus / 5;
97
98                vec![
99                    Stage {
100                        duration: format!("{}s", step_duration),
101                        target: step_vus,
102                    },
103                    Stage {
104                        duration: format!("{}s", step_duration),
105                        target: step_vus * 2,
106                    },
107                    Stage {
108                        duration: format!("{}s", step_duration),
109                        target: step_vus * 3,
110                    },
111                    Stage {
112                        duration: format!("{}s", step_duration),
113                        target: step_vus * 4,
114                    },
115                    Stage {
116                        duration: format!("{}s", step_duration),
117                        target: max_vus,
118                    },
119                    Stage {
120                        duration: format!("{}s", step_duration),
121                        target: 0,
122                    },
123                ]
124            }
125            Self::Soak => {
126                // Minimal ramp-up, long sustained load
127                let ramp_duration = duration_secs / 20;
128                let sustain_duration = duration_secs - (ramp_duration * 2);
129
130                vec![
131                    Stage {
132                        duration: format!("{}s", ramp_duration),
133                        target: max_vus,
134                    },
135                    Stage {
136                        duration: format!("{}s", sustain_duration),
137                        target: max_vus,
138                    },
139                    Stage {
140                        duration: format!("{}s", ramp_duration),
141                        target: 0,
142                    },
143                ]
144            }
145        }
146    }
147
148    /// Get description of this scenario
149    pub fn description(&self) -> &str {
150        match self {
151            Self::Constant => "Constant load with steady VUs",
152            Self::RampUp => "Gradually increase load to target VUs",
153            Self::Spike => "Sudden spike in load to test system resilience",
154            Self::Stress => "Continuously increase load to find breaking point",
155            Self::Soak => "Sustained load over extended period",
156        }
157    }
158}
159
160/// A k6 load stage
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct Stage {
163    pub duration: String,
164    pub target: u32,
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use std::str::FromStr;
171
172    #[test]
173    fn test_scenario_from_str() {
174        assert_eq!(LoadScenario::from_str("constant").unwrap(), LoadScenario::Constant);
175        assert_eq!(LoadScenario::from_str("ramp-up").unwrap(), LoadScenario::RampUp);
176        assert_eq!(LoadScenario::from_str("spike").unwrap(), LoadScenario::Spike);
177        assert!(LoadScenario::from_str("unknown").is_err());
178    }
179
180    #[test]
181    fn test_constant_stages() {
182        let scenario = LoadScenario::Constant;
183        let stages = scenario.generate_stages(60, 10);
184        assert_eq!(stages.len(), 1);
185        assert_eq!(stages[0].target, 10);
186    }
187
188    #[test]
189    fn test_rampup_stages() {
190        let scenario = LoadScenario::RampUp;
191        let stages = scenario.generate_stages(120, 100);
192        assert!(stages.len() >= 3);
193        assert_eq!(stages.last().unwrap().target, 0);
194    }
195
196    #[test]
197    fn test_spike_stages() {
198        let scenario = LoadScenario::Spike;
199        let stages = scenario.generate_stages(100, 100);
200        assert!(stages.len() >= 3);
201        // Check that there's a spike
202        let max_stage = stages.iter().max_by_key(|s| s.target).unwrap();
203        assert_eq!(max_stage.target, 100);
204    }
205}