Skip to main content

rusmes_loadtest/
config.rs

1//! Load test configuration
2
3use crate::scenarios::ScenarioType;
4use serde::{Deserialize, Serialize};
5use std::time::Duration;
6
7/// Protocol to test
8#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
9pub enum Protocol {
10    /// SMTP protocol
11    Smtp,
12    /// IMAP protocol
13    Imap,
14    /// JMAP protocol
15    Jmap,
16    /// POP3 protocol
17    Pop3,
18    /// Mixed protocols
19    Mixed,
20}
21
22/// Message size configuration
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub enum MessageSize {
25    /// Fixed size in bytes
26    Fixed(usize),
27    /// Random size between min and max
28    Random { min: usize, max: usize },
29}
30
31impl MessageSize {
32    /// Get a size value
33    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/// Message content type
42#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
43pub enum MessageContent {
44    /// Random text
45    Random,
46    /// Template-based
47    Template,
48    /// Real-world simulated
49    RealWorld,
50}
51
52/// Load test configuration
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct LoadTestConfig {
55    /// Target host to test
56    pub target_host: String,
57
58    /// Target port
59    pub target_port: u16,
60
61    /// Protocol to test
62    pub protocol: Protocol,
63
64    /// Test scenario to run
65    pub scenario: ScenarioType,
66
67    /// Test duration in seconds
68    pub duration_secs: u64,
69
70    /// Number of concurrent workers
71    pub concurrency: usize,
72
73    /// Target message rate (messages per second)
74    pub message_rate: u64,
75
76    /// Ramp-up duration in seconds (gradual increase to target rate)
77    pub ramp_up_secs: u64,
78
79    /// Message size configuration
80    pub message_size: MessageSize,
81
82    /// Message content type
83    pub message_content: MessageContent,
84
85    /// Minimum message size in bytes (deprecated, use message_size)
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub message_size_min: Option<usize>,
88
89    /// Maximum message size in bytes (deprecated, use message_size)
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub message_size_max: Option<usize>,
92
93    /// Output file for JSON report
94    pub output_json: Option<String>,
95
96    /// Output file for HTML report
97    pub output_html: Option<String>,
98
99    /// Output file for CSV report
100    pub output_csv: Option<String>,
101
102    /// Enable Prometheus metrics export
103    pub prometheus_export: bool,
104
105    /// Prometheus export port
106    pub prometheus_port: u16,
107
108    /// Mixed protocol weights (SMTP, IMAP, JMAP, POP3)
109    pub mixed_weights: Option<(u8, u8, u8, u8)>,
110}
111
112impl LoadTestConfig {
113    /// Get ramp-up duration
114    pub fn ramp_up_duration(&self) -> Duration {
115        Duration::from_secs(self.ramp_up_secs)
116    }
117
118    /// Get test duration
119    pub fn test_duration(&self) -> Duration {
120        Duration::from_secs(self.duration_secs)
121    }
122
123    /// Get message size range
124    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    /// Validate the configuration
159    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}