1use std::sync::Arc;
6use std::time::{Duration, Instant};
7use thiserror::Error;
8use tokio::sync::Mutex;
9
10#[derive(Debug, Error)]
12pub enum LoadTestError {
13 #[error("Test failed: {0}")]
14 TestFailed(String),
15
16 #[error("Timeout: {0}")]
17 Timeout(String),
18}
19
20#[derive(Debug, Clone)]
22pub struct LoadTestStats {
23 pub total_requests: u64,
25
26 pub successful: u64,
28
29 pub failed: u64,
31
32 pub duration: Duration,
34
35 pub min_response_time: Duration,
37
38 pub max_response_time: Duration,
40
41 pub avg_response_time: Duration,
43
44 pub median_response_time: Duration,
46
47 pub p95_response_time: Duration,
49
50 pub p99_response_time: Duration,
52
53 pub rps: f64,
55}
56
57impl LoadTestStats {
58 pub fn from_response_times(
60 response_times: &[Duration],
61 failed: u64,
62 total_duration: Duration,
63 ) -> Self {
64 let mut sorted = response_times.to_vec();
65 sorted.sort();
66
67 let total = response_times.len() as u64;
68 let sum: Duration = response_times.iter().sum();
69
70 let min = sorted.first().copied().unwrap_or_default();
71 let max = sorted.last().copied().unwrap_or_default();
72 let avg = if !sorted.is_empty() {
73 sum / sorted.len() as u32
74 } else {
75 Duration::default()
76 };
77
78 let median = if !sorted.is_empty() {
79 sorted[sorted.len() / 2]
80 } else {
81 Duration::default()
82 };
83
84 let p95_idx = (sorted.len() as f64 * 0.95) as usize;
85 let p95 = sorted.get(p95_idx).copied().unwrap_or(max);
86
87 let p99_idx = (sorted.len() as f64 * 0.99) as usize;
88 let p99 = sorted.get(p99_idx).copied().unwrap_or(max);
89
90 let rps = if total_duration.as_secs_f64() > 0.0 {
91 total as f64 / total_duration.as_secs_f64()
92 } else {
93 0.0
94 };
95
96 Self {
97 total_requests: total + failed,
98 successful: total,
99 failed,
100 duration: total_duration,
101 min_response_time: min,
102 max_response_time: max,
103 avg_response_time: avg,
104 median_response_time: median,
105 p95_response_time: p95,
106 p99_response_time: p99,
107 rps,
108 }
109 }
110
111 pub fn print(&self) {
113 println!("\n========== Load Test Results ==========");
114 println!("Total Requests: {}", self.total_requests);
115 println!("Successful: {}", self.successful);
116 println!("Failed: {}", self.failed);
117 println!("Duration: {:.2}s", self.duration.as_secs_f64());
118 println!("Requests/sec: {:.2}", self.rps);
119 println!("\nResponse Times:");
120 println!(
121 " Min: {:.2}ms",
122 self.min_response_time.as_millis()
123 );
124 println!(
125 " Avg: {:.2}ms",
126 self.avg_response_time.as_millis()
127 );
128 println!(
129 " Median (p50): {:.2}ms",
130 self.median_response_time.as_millis()
131 );
132 println!(
133 " p95: {:.2}ms",
134 self.p95_response_time.as_millis()
135 );
136 println!(
137 " p99: {:.2}ms",
138 self.p99_response_time.as_millis()
139 );
140 println!(
141 " Max: {:.2}ms",
142 self.max_response_time.as_millis()
143 );
144 println!("=======================================\n");
145 }
146}
147
148#[derive(Debug, Clone)]
150pub struct LoadTestConfig {
151 pub concurrency: usize,
153
154 pub total_requests: u64,
156
157 pub duration: Option<Duration>,
159
160 pub rate_limit: Option<f64>,
162
163 pub timeout: Duration,
165}
166
167impl LoadTestConfig {
168 pub fn new(concurrency: usize, total_requests: u64) -> Self {
178 Self {
179 concurrency,
180 total_requests,
181 duration: None,
182 rate_limit: None,
183 timeout: Duration::from_secs(30),
184 }
185 }
186
187 pub fn with_duration(mut self, duration: Duration) -> Self {
189 self.duration = Some(duration);
190 self
191 }
192
193 pub fn with_rate_limit(mut self, rps: f64) -> Self {
195 self.rate_limit = Some(rps);
196 self
197 }
198
199 pub fn with_timeout(mut self, timeout: Duration) -> Self {
201 self.timeout = timeout;
202 self
203 }
204}
205
206impl Default for LoadTestConfig {
207 fn default() -> Self {
208 Self::new(10, 100)
209 }
210}
211
212pub struct LoadTestRunner<F, Fut>
214where
215 F: Fn() -> Fut + Send + Sync + Clone + 'static,
216 Fut: std::future::Future<Output = Result<(), LoadTestError>> + Send + 'static,
217{
218 config: LoadTestConfig,
219 test_fn: Arc<F>,
220}
221
222impl<F, Fut> LoadTestRunner<F, Fut>
223where
224 F: Fn() -> Fut + Send + Sync + Clone + 'static,
225 Fut: std::future::Future<Output = Result<(), LoadTestError>> + Send + 'static,
226{
227 pub fn new(config: LoadTestConfig, test_fn: F) -> Self {
241 Self {
242 config,
243 test_fn: Arc::new(test_fn),
244 }
245 }
246
247 pub async fn run(&self) -> Result<LoadTestStats, LoadTestError> {
249 let start_time = Instant::now();
250 let response_times = Arc::new(Mutex::new(Vec::new()));
251 let failed_count = Arc::new(Mutex::new(0u64));
252
253 let mut handles = vec![];
254
255 let concurrency = self.config.concurrency as u64;
256
257 let min_interval = self
262 .config
263 .rate_limit
264 .filter(|rps| *rps > 0.0)
265 .map(|rps| Duration::from_secs_f64(concurrency.max(1) as f64 / rps));
266
267 for worker_idx in 0..self.config.concurrency {
268 let test_fn = self.test_fn.clone();
269 let response_times = response_times.clone();
270 let failed_count = failed_count.clone();
271 let timeout = self.config.timeout;
272 let duration = self.config.duration;
273
274 let requests_for_this_worker = if duration.is_some() {
282 None
283 } else {
284 let base = self.config.total_requests / concurrency.max(1);
285 let remainder = self.config.total_requests % concurrency.max(1);
286 Some(base + u64::from((worker_idx as u64) < remainder))
287 };
288
289 let handle = tokio::spawn(async move {
290 let worker_start = Instant::now();
291 let mut request_count = 0u64;
292
293 let should_stop = |request_count: u64| -> bool {
299 if let Some(duration) = duration {
300 worker_start.elapsed() >= duration
301 } else if let Some(max_requests) = requests_for_this_worker {
302 request_count >= max_requests
303 } else {
304 false
305 }
306 };
307
308 loop {
309 if should_stop(request_count) {
311 break;
312 }
313
314 let req_start = Instant::now();
316 let result = tokio::time::timeout(timeout, test_fn()).await;
317
318 match result {
319 Ok(Ok(())) => {
320 let elapsed = req_start.elapsed();
321 response_times.lock().await.push(elapsed);
322 }
323 _ => {
324 *failed_count.lock().await += 1;
325 }
326 }
327
328 request_count += 1;
329
330 if !should_stop(request_count)
338 && let Some(interval) = min_interval
339 {
340 let elapsed = req_start.elapsed();
341 if elapsed < interval {
342 tokio::time::sleep(interval - elapsed).await;
343 }
344 }
345 }
346 });
347
348 handles.push(handle);
349 }
350
351 for handle in handles {
353 let _ = handle.await;
354 }
355
356 let total_duration = start_time.elapsed();
357 let response_times = response_times.lock().await;
358 let failed = *failed_count.lock().await;
359
360 Ok(LoadTestStats::from_response_times(
361 &response_times,
362 failed,
363 total_duration,
364 ))
365 }
366}
367
368pub struct StressTestRunner<F, Fut>
370where
371 F: Fn() -> Fut + Send + Sync + Clone + 'static,
372 Fut: std::future::Future<Output = Result<(), LoadTestError>> + Send + 'static,
373{
374 initial_concurrency: usize,
375 max_concurrency: usize,
376 step_size: usize,
377 step_duration: Duration,
378 test_fn: Arc<F>,
379}
380
381impl<F, Fut> StressTestRunner<F, Fut>
382where
383 F: Fn() -> Fut + Send + Sync + Clone + 'static,
384 Fut: std::future::Future<Output = Result<(), LoadTestError>> + Send + 'static,
385{
386 pub fn new(
400 initial_concurrency: usize,
401 max_concurrency: usize,
402 step_size: usize,
403 step_duration: Duration,
404 test_fn: F,
405 ) -> Self {
406 Self {
407 initial_concurrency,
408 max_concurrency,
409 step_size,
410 step_duration,
411 test_fn: Arc::new(test_fn),
412 }
413 }
414
415 pub async fn run(&self) -> Result<Vec<(usize, LoadTestStats)>, LoadTestError> {
417 let mut results = vec![];
418 let mut concurrency = self.initial_concurrency;
419
420 println!("\n========== Stress Test Starting ==========");
421 println!("Initial Concurrency: {}", self.initial_concurrency);
422 println!("Max Concurrency: {}", self.max_concurrency);
423 println!("Step Size: {}", self.step_size);
424 println!(
425 "Step Duration: {:.0}s",
426 self.step_duration.as_secs_f64()
427 );
428 println!("==========================================\n");
429
430 while concurrency <= self.max_concurrency {
431 println!("Testing with {} concurrent requests...", concurrency);
432
433 let config =
434 LoadTestConfig::new(concurrency, u64::MAX).with_duration(self.step_duration);
435
436 let runner = LoadTestRunner::new(config, self.test_fn.as_ref().clone());
437 let stats = runner.run().await?;
438
439 println!(
440 " RPS: {:.2}, Avg: {:.2}ms, p95: {:.2}ms",
441 stats.rps,
442 stats.avg_response_time.as_millis(),
443 stats.p95_response_time.as_millis()
444 );
445
446 results.push((concurrency, stats));
447 concurrency += self.step_size;
448 }
449
450 println!("\n========== Stress Test Complete ==========\n");
451
452 Ok(results)
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 #[test]
461 fn test_load_test_config() {
462 let config = LoadTestConfig::new(10, 100)
463 .with_rate_limit(50.0)
464 .with_timeout(Duration::from_secs(5));
465
466 assert_eq!(config.concurrency, 10);
467 assert_eq!(config.total_requests, 100);
468 assert_eq!(config.rate_limit, Some(50.0));
469 }
470
471 #[test]
472 fn test_load_test_stats() {
473 let response_times = vec![
474 Duration::from_millis(100),
475 Duration::from_millis(200),
476 Duration::from_millis(150),
477 Duration::from_millis(300),
478 Duration::from_millis(250),
479 ];
480
481 let stats = LoadTestStats::from_response_times(&response_times, 0, Duration::from_secs(1));
482
483 assert_eq!(stats.total_requests, 5);
484 assert_eq!(stats.successful, 5);
485 assert_eq!(stats.failed, 0);
486 assert_eq!(stats.min_response_time, Duration::from_millis(100));
487 assert_eq!(stats.max_response_time, Duration::from_millis(300));
488 }
489
490 #[tokio::test]
491 async fn test_load_test_runner() {
492 let config = LoadTestConfig::new(2, 10);
493
494 let runner = LoadTestRunner::new(config, || async {
495 tokio::time::sleep(Duration::from_millis(10)).await;
496 Ok(())
497 });
498
499 let stats = runner.run().await.unwrap();
500 assert_eq!(stats.total_requests, 10);
501 assert_eq!(stats.successful, 10);
502 }
503
504 #[tokio::test]
505 async fn run_executes_exactly_total_requests_when_not_evenly_divisible() {
506 use std::sync::atomic::{AtomicU64, Ordering};
510 static COUNTER: AtomicU64 = AtomicU64::new(0);
511 COUNTER.store(0, Ordering::SeqCst);
512
513 let config = LoadTestConfig::new(3, 100);
514 let runner = LoadTestRunner::new(config, || async {
515 COUNTER.fetch_add(1, Ordering::SeqCst);
516 Ok(())
517 });
518
519 let stats = runner.run().await.unwrap();
520 assert_eq!(COUNTER.load(Ordering::SeqCst), 100);
521 assert_eq!(stats.total_requests, 100);
522 }
523
524 #[tokio::test]
525 async fn run_executes_exactly_total_requests_when_concurrency_exceeds_total() {
526 use std::sync::atomic::{AtomicU64, Ordering};
529 static COUNTER: AtomicU64 = AtomicU64::new(0);
530 COUNTER.store(0, Ordering::SeqCst);
531
532 let config = LoadTestConfig::new(5, 2);
533 let runner = LoadTestRunner::new(config, || async {
534 COUNTER.fetch_add(1, Ordering::SeqCst);
535 Ok(())
536 });
537
538 let stats = runner.run().await.unwrap();
539 assert_eq!(COUNTER.load(Ordering::SeqCst), 2);
540 assert_eq!(stats.total_requests, 2);
541 }
542
543 #[tokio::test]
544 async fn run_honors_rate_limit_by_pacing_requests() {
545 let config = LoadTestConfig::new(1, 4).with_rate_limit(20.0);
557 let runner = LoadTestRunner::new(config, || async { Ok(()) });
558
559 let start = Instant::now();
560 let stats = runner.run().await.unwrap();
561 let elapsed = start.elapsed();
562
563 assert_eq!(stats.total_requests, 4);
564 assert!(
565 elapsed >= Duration::from_millis(140) && elapsed < Duration::from_millis(195),
566 "expected pacing to take ~150ms for 3 gaps at 50ms (not ~200ms for 4 sleeps), took {elapsed:?}"
567 );
568 }
569}