1use std::collections::VecDeque;
23use std::time::{Duration, Instant};
24
25#[derive(Debug, Clone)]
27pub enum LoadPattern {
28 Constant(usize),
30 Ramp { min: usize, max: usize },
32 Step {
34 steps: Vec<(usize, Duration)>, },
36 Spike {
38 normal_rate: usize,
39 spike_rate: usize,
40 spike_duration: Duration,
41 spike_interval: Duration,
42 },
43 Random { min: usize, max: usize },
45}
46
47#[derive(Debug, Clone)]
49pub struct LoadTestConfig {
50 pub duration_secs: u64,
52 pub pattern: LoadPattern,
54 pub block_size_bytes: usize,
56 pub concurrent_requests: usize,
58}
59
60impl Default for LoadTestConfig {
61 fn default() -> Self {
62 Self {
63 duration_secs: 60,
64 pattern: LoadPattern::Constant(100),
65 block_size_bytes: 1024,
66 concurrent_requests: 10,
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct LoadTestStats {
74 pub total_requests: usize,
76 pub successful_responses: usize,
78 pub failures: usize,
80 pub bytes_transferred: u64,
82 pub duration: Duration,
84 pub avg_latency_ms: f64,
86 pub p50_latency_ms: f64,
88 pub p95_latency_ms: f64,
90 pub p99_latency_ms: f64,
92 pub requests_per_second: f64,
94 pub throughput_bps: f64,
96}
97
98impl Default for LoadTestStats {
99 fn default() -> Self {
100 Self {
101 total_requests: 0,
102 successful_responses: 0,
103 failures: 0,
104 bytes_transferred: 0,
105 duration: Duration::from_secs(0),
106 avg_latency_ms: 0.0,
107 p50_latency_ms: 0.0,
108 p95_latency_ms: 0.0,
109 p99_latency_ms: 0.0,
110 requests_per_second: 0.0,
111 throughput_bps: 0.0,
112 }
113 }
114}
115
116impl std::fmt::Display for LoadTestStats {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 writeln!(f, "Load Test Results:")?;
119 writeln!(f, " Duration: {:?}", self.duration)?;
120 writeln!(f, " Total Requests: {}", self.total_requests)?;
121 writeln!(f, " Successful: {}", self.successful_responses)?;
122 writeln!(f, " Failures: {}", self.failures)?;
123 writeln!(f, " Bytes Transferred: {}", self.bytes_transferred)?;
124 writeln!(f, " Requests/sec: {:.2}", self.requests_per_second)?;
125 writeln!(
126 f,
127 " Throughput: {:.2} MB/s",
128 self.throughput_bps / 1_000_000.0
129 )?;
130 writeln!(f, " Avg Latency: {:.2}ms", self.avg_latency_ms)?;
131 writeln!(f, " p50 Latency: {:.2}ms", self.p50_latency_ms)?;
132 writeln!(f, " p95 Latency: {:.2}ms", self.p95_latency_ms)?;
133 writeln!(f, " p99 Latency: {:.2}ms", self.p99_latency_ms)?;
134 Ok(())
135 }
136}
137
138pub struct LoadTester {
140 config: LoadTestConfig,
141 stats: LoadTestStats,
142 latencies: VecDeque<u64>,
143 start_time: Option<Instant>,
144}
145
146impl LoadTester {
147 pub fn new(config: LoadTestConfig) -> Self {
149 Self {
150 config,
151 stats: LoadTestStats::default(),
152 latencies: VecDeque::new(),
153 start_time: None,
154 }
155 }
156
157 pub fn start(&mut self) {
159 self.start_time = Some(Instant::now());
160 self.stats = LoadTestStats::default();
161 self.latencies.clear();
162 }
163
164 pub fn record_success(&mut self, latency_ms: u64, bytes: usize) {
166 self.stats.total_requests += 1;
167 self.stats.successful_responses += 1;
168 self.stats.bytes_transferred += bytes as u64;
169 self.latencies.push_back(latency_ms);
170
171 if self.latencies.len() > 10000 {
173 self.latencies.pop_front();
174 }
175 }
176
177 pub fn record_failure(&mut self) {
179 self.stats.total_requests += 1;
180 self.stats.failures += 1;
181 }
182
183 pub fn stats(&self) -> &LoadTestStats {
185 &self.stats
186 }
187
188 pub fn finalize(&mut self) -> LoadTestStats {
190 if let Some(start) = self.start_time {
191 self.stats.duration = start.elapsed();
192 }
193
194 if !self.latencies.is_empty() {
196 let mut sorted: Vec<u64> = self.latencies.iter().copied().collect();
197 sorted.sort_unstable();
198
199 let sum: u64 = sorted.iter().sum();
200 self.stats.avg_latency_ms = sum as f64 / sorted.len() as f64;
201
202 let p50_idx = (sorted.len() as f64 * 0.50) as usize;
203 let p95_idx = (sorted.len() as f64 * 0.95) as usize;
204 let p99_idx = (sorted.len() as f64 * 0.99) as usize;
205
206 self.stats.p50_latency_ms = sorted.get(p50_idx).copied().unwrap_or(0) as f64;
207 self.stats.p95_latency_ms = sorted.get(p95_idx).copied().unwrap_or(0) as f64;
208 self.stats.p99_latency_ms = sorted.get(p99_idx).copied().unwrap_or(0) as f64;
209 }
210
211 let duration_secs = self.stats.duration.as_secs_f64();
213 if duration_secs > 0.0 {
214 self.stats.requests_per_second = self.stats.total_requests as f64 / duration_secs;
215 self.stats.throughput_bps = self.stats.bytes_transferred as f64 / duration_secs;
216 }
217
218 self.stats.clone()
219 }
220
221 pub fn get_target_rate(&self, elapsed: Duration) -> usize {
223 match &self.config.pattern {
224 LoadPattern::Constant(rate) => *rate,
225 LoadPattern::Ramp { min, max } => {
226 let progress = elapsed.as_secs_f64() / self.config.duration_secs as f64;
227 let range = (*max - *min) as f64;
228 (*min as f64 + range * progress) as usize
229 }
230 LoadPattern::Step { steps } => {
231 let mut accumulated = Duration::from_secs(0);
232 for (rate, duration) in steps {
233 accumulated += *duration;
234 if elapsed < accumulated {
235 return *rate;
236 }
237 }
238 steps.last().map(|(rate, _)| *rate).unwrap_or(0)
239 }
240 LoadPattern::Spike {
241 normal_rate,
242 spike_rate,
243 spike_duration,
244 spike_interval,
245 } => {
246 let cycle_time = elapsed.as_secs_f64() % spike_interval.as_secs_f64();
247 if cycle_time < spike_duration.as_secs_f64() {
248 *spike_rate
249 } else {
250 *normal_rate
251 }
252 }
253 LoadPattern::Random { min, max } => {
254 let seed = elapsed.as_millis() as usize;
256 min + (seed % (max - min + 1))
257 }
258 }
259 }
260
261 pub fn config(&self) -> &LoadTestConfig {
263 &self.config
264 }
265
266 pub fn reset(&mut self) {
268 self.stats = LoadTestStats::default();
269 self.latencies.clear();
270 self.start_time = None;
271 }
272}
273
274pub struct LoadTestConfigBuilder {
276 config: LoadTestConfig,
277}
278
279impl LoadTestConfigBuilder {
280 pub fn new() -> Self {
282 Self {
283 config: LoadTestConfig::default(),
284 }
285 }
286
287 pub fn duration_secs(mut self, secs: u64) -> Self {
289 self.config.duration_secs = secs;
290 self
291 }
292
293 pub fn pattern(mut self, pattern: LoadPattern) -> Self {
295 self.config.pattern = pattern;
296 self
297 }
298
299 pub fn block_size_bytes(mut self, bytes: usize) -> Self {
301 self.config.block_size_bytes = bytes;
302 self
303 }
304
305 pub fn concurrent_requests(mut self, count: usize) -> Self {
307 self.config.concurrent_requests = count;
308 self
309 }
310
311 pub fn build(self) -> LoadTestConfig {
313 self.config
314 }
315}
316
317impl Default for LoadTestConfigBuilder {
318 fn default() -> Self {
319 Self::new()
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn test_load_tester_creation() {
329 let config = LoadTestConfig::default();
330 let tester = LoadTester::new(config);
331 assert_eq!(tester.stats().total_requests, 0);
332 }
333
334 #[test]
335 fn test_record_success() {
336 let config = LoadTestConfig::default();
337 let mut tester = LoadTester::new(config);
338
339 tester.start();
340 tester.record_success(50, 1024);
341
342 assert_eq!(tester.stats().total_requests, 1);
343 assert_eq!(tester.stats().successful_responses, 1);
344 assert_eq!(tester.stats().bytes_transferred, 1024);
345 }
346
347 #[test]
348 fn test_record_failure() {
349 let config = LoadTestConfig::default();
350 let mut tester = LoadTester::new(config);
351
352 tester.start();
353 tester.record_failure();
354
355 assert_eq!(tester.stats().total_requests, 1);
356 assert_eq!(tester.stats().failures, 1);
357 }
358
359 #[test]
360 fn test_finalize_stats() {
361 let config = LoadTestConfig::default();
362 let mut tester = LoadTester::new(config);
363
364 tester.start();
365 tester.record_success(50, 1024);
366 tester.record_success(60, 1024);
367 tester.record_success(70, 1024);
368
369 std::thread::sleep(Duration::from_millis(1));
371
372 let stats = tester.finalize();
373 assert_eq!(stats.total_requests, 3);
374 assert!(stats.avg_latency_ms > 0.0);
375 assert!(stats.throughput_bps > 0.0);
376 }
377
378 #[test]
379 fn test_constant_load_pattern() {
380 let config = LoadTestConfig {
381 pattern: LoadPattern::Constant(100),
382 ..Default::default()
383 };
384 let tester = LoadTester::new(config);
385
386 assert_eq!(tester.get_target_rate(Duration::from_secs(0)), 100);
387 assert_eq!(tester.get_target_rate(Duration::from_secs(30)), 100);
388 }
389
390 #[test]
391 fn test_ramp_load_pattern() {
392 let config = LoadTestConfig {
393 duration_secs: 10,
394 pattern: LoadPattern::Ramp { min: 10, max: 100 },
395 ..Default::default()
396 };
397 let tester = LoadTester::new(config);
398
399 let rate_start = tester.get_target_rate(Duration::from_secs(0));
400 let rate_end = tester.get_target_rate(Duration::from_secs(10));
401
402 assert_eq!(rate_start, 10);
403 assert_eq!(rate_end, 100);
404 }
405
406 #[test]
407 fn test_step_load_pattern() {
408 let config = LoadTestConfig {
409 pattern: LoadPattern::Step {
410 steps: vec![
411 (10, Duration::from_secs(5)),
412 (50, Duration::from_secs(5)),
413 (100, Duration::from_secs(5)),
414 ],
415 },
416 ..Default::default()
417 };
418 let tester = LoadTester::new(config);
419
420 assert_eq!(tester.get_target_rate(Duration::from_secs(2)), 10);
421 assert_eq!(tester.get_target_rate(Duration::from_secs(7)), 50);
422 assert_eq!(tester.get_target_rate(Duration::from_secs(12)), 100);
423 }
424
425 #[test]
426 fn test_spike_load_pattern() {
427 let config = LoadTestConfig {
428 pattern: LoadPattern::Spike {
429 normal_rate: 10,
430 spike_rate: 100,
431 spike_duration: Duration::from_secs(2),
432 spike_interval: Duration::from_secs(10),
433 },
434 ..Default::default()
435 };
436 let tester = LoadTester::new(config);
437
438 assert_eq!(tester.get_target_rate(Duration::from_secs(1)), 100); assert_eq!(tester.get_target_rate(Duration::from_secs(5)), 10); }
441
442 #[test]
443 fn test_config_builder() {
444 let config = LoadTestConfigBuilder::new()
445 .duration_secs(30)
446 .pattern(LoadPattern::Constant(50))
447 .block_size_bytes(2048)
448 .concurrent_requests(20)
449 .build();
450
451 assert_eq!(config.duration_secs, 30);
452 assert_eq!(config.block_size_bytes, 2048);
453 assert_eq!(config.concurrent_requests, 20);
454 }
455
456 #[test]
457 fn test_reset() {
458 let config = LoadTestConfig::default();
459 let mut tester = LoadTester::new(config);
460
461 tester.start();
462 tester.record_success(50, 1024);
463
464 assert_eq!(tester.stats().total_requests, 1);
465
466 tester.reset();
467 assert_eq!(tester.stats().total_requests, 0);
468 }
469
470 #[test]
471 fn test_percentile_calculation() {
472 let config = LoadTestConfig::default();
473 let mut tester = LoadTester::new(config);
474
475 tester.start();
476 for i in 1..=100 {
477 tester.record_success(i, 1024);
478 }
479
480 let stats = tester.finalize();
481 assert!(stats.p50_latency_ms >= 45.0 && stats.p50_latency_ms <= 55.0);
482 assert!(stats.p95_latency_ms >= 90.0 && stats.p95_latency_ms <= 100.0);
483 assert!(stats.p99_latency_ms >= 95.0 && stats.p99_latency_ms <= 100.0);
484 }
485
486 #[test]
487 fn test_stats_display() {
488 let stats = LoadTestStats {
489 total_requests: 100,
490 successful_responses: 95,
491 failures: 5,
492 bytes_transferred: 102400,
493 duration: Duration::from_secs(10),
494 avg_latency_ms: 50.0,
495 p50_latency_ms: 45.0,
496 p95_latency_ms: 90.0,
497 p99_latency_ms: 95.0,
498 requests_per_second: 10.0,
499 throughput_bps: 10240.0,
500 };
501
502 let display = format!("{}", stats);
503 assert!(display.contains("Total Requests: 100"));
504 assert!(display.contains("Successful: 95"));
505 }
506}