1use crate::scenarios::ScenarioType;
4use serde::{Deserialize, Serialize};
5use std::time::Duration;
6
7#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
9pub enum Protocol {
10 Smtp,
12 Imap,
14 Jmap,
16 Pop3,
18 Mixed,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub enum MessageSize {
25 Fixed(usize),
27 Random { min: usize, max: usize },
29}
30
31impl MessageSize {
32 pub fn get(&self) -> (usize, usize) {
34 match self {
35 MessageSize::Fixed(size) => (*size, *size),
36 MessageSize::Random { min, max } => (*min, *max),
37 }
38 }
39}
40
41#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
43pub enum MessageContent {
44 Random,
46 Template,
48 RealWorld,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct LoadTestConfig {
55 pub target_host: String,
57
58 pub target_port: u16,
60
61 pub protocol: Protocol,
63
64 pub scenario: ScenarioType,
66
67 pub duration_secs: u64,
69
70 pub concurrency: usize,
72
73 pub message_rate: u64,
75
76 pub ramp_up_secs: u64,
78
79 pub message_size: MessageSize,
81
82 pub message_content: MessageContent,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub message_size_min: Option<usize>,
88
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub message_size_max: Option<usize>,
92
93 pub output_json: Option<String>,
95
96 pub output_html: Option<String>,
98
99 pub output_csv: Option<String>,
101
102 pub prometheus_export: bool,
104
105 pub prometheus_port: u16,
107
108 pub mixed_weights: Option<(u8, u8, u8, u8)>,
110}
111
112impl LoadTestConfig {
113 pub fn ramp_up_duration(&self) -> Duration {
115 Duration::from_secs(self.ramp_up_secs)
116 }
117
118 pub fn test_duration(&self) -> Duration {
120 Duration::from_secs(self.duration_secs)
121 }
122
123 pub fn message_size_range(&self) -> (usize, usize) {
125 self.message_size.get()
126 }
127}
128
129impl Default for LoadTestConfig {
130 fn default() -> Self {
131 Self {
132 target_host: "localhost".to_string(),
133 target_port: 25,
134 protocol: Protocol::Smtp,
135 scenario: ScenarioType::SmtpThroughput,
136 duration_secs: 60,
137 concurrency: 10,
138 message_rate: 100,
139 ramp_up_secs: 0,
140 message_size: MessageSize::Random {
141 min: 1024,
142 max: 102400,
143 },
144 message_content: MessageContent::Random,
145 message_size_min: None,
146 message_size_max: None,
147 output_json: None,
148 output_html: None,
149 output_csv: None,
150 prometheus_export: false,
151 prometheus_port: 9090,
152 mixed_weights: None,
153 }
154 }
155}
156
157impl LoadTestConfig {
158 pub fn validate(&self) -> Result<(), String> {
160 if self.target_host.is_empty() {
161 return Err("Target host cannot be empty".to_string());
162 }
163
164 if self.target_port == 0 {
165 return Err("Target port must be greater than 0".to_string());
166 }
167
168 if self.duration_secs == 0 {
169 return Err("Duration must be greater than 0".to_string());
170 }
171
172 if self.concurrency == 0 {
173 return Err("Concurrency must be greater than 0".to_string());
174 }
175
176 match &self.message_size {
177 MessageSize::Fixed(size) if *size == 0 => {
178 return Err("Message size must be greater than 0".to_string());
179 }
180 MessageSize::Random { min, max } if min > max => {
181 return Err("Min message size cannot be greater than max".to_string());
182 }
183 MessageSize::Random { min, .. } if *min == 0 => {
184 return Err("Min message size must be greater than 0".to_string());
185 }
186 _ => {}
187 }
188
189 if self.protocol == Protocol::Mixed && self.mixed_weights.is_none() {
190 return Err("Mixed protocol requires weights (smtp, imap, jmap, pop3)".to_string());
191 }
192
193 if let Some((smtp, imap, jmap, pop3)) = self.mixed_weights {
194 if smtp + imap + jmap + pop3 == 0 {
195 return Err("At least one protocol weight must be non-zero".to_string());
196 }
197 }
198
199 Ok(())
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_default_config_is_valid() {
209 let config = LoadTestConfig::default();
210 assert!(config.validate().is_ok());
211 }
212
213 #[test]
214 fn test_empty_host_is_invalid() {
215 let config = LoadTestConfig {
216 target_host: "".to_string(),
217 ..LoadTestConfig::default()
218 };
219 assert!(config.validate().is_err());
220 }
221
222 #[test]
223 fn test_zero_port_is_invalid() {
224 let config = LoadTestConfig {
225 target_port: 0,
226 ..LoadTestConfig::default()
227 };
228 assert!(config.validate().is_err());
229 }
230
231 #[test]
232 fn test_zero_duration_is_invalid() {
233 let config = LoadTestConfig {
234 duration_secs: 0,
235 ..LoadTestConfig::default()
236 };
237 assert!(config.validate().is_err());
238 }
239
240 #[test]
241 fn test_zero_concurrency_is_invalid() {
242 let config = LoadTestConfig {
243 concurrency: 0,
244 ..LoadTestConfig::default()
245 };
246 assert!(config.validate().is_err());
247 }
248
249 #[test]
250 fn test_invalid_message_sizes() {
251 let config = LoadTestConfig {
252 message_size: MessageSize::Random {
253 min: 10000,
254 max: 1000,
255 },
256 ..LoadTestConfig::default()
257 };
258 assert!(config.validate().is_err());
259 }
260}