use foxy::Foxy;
use serde_json::json;
use serial_test::serial;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Semaphore;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
mod common;
use common::{TestConfigProvider, TestRoute, init_test_logging};
#[derive(Debug, Clone)]
pub struct LoadTestConfig {
pub concurrent_requests: usize,
pub total_requests: usize,
pub duration: Duration,
pub request_timeout: Duration,
}
impl Default for LoadTestConfig {
fn default() -> Self {
Self {
concurrent_requests: 10,
total_requests: 1000,
duration: Duration::from_secs(30),
request_timeout: Duration::from_secs(5),
}
}
}
#[derive(Debug)]
pub struct LoadTestResults {
pub total_requests: usize,
pub successful_requests: usize,
pub failed_requests: usize,
pub total_duration: Duration,
pub requests_per_second: f64,
pub avg_response_time: Duration,
pub min_response_time: Duration,
pub max_response_time: Duration,
pub p95_response_time: Duration,
pub p99_response_time: Duration,
}
impl LoadTestResults {
pub fn print_summary(&self) {
println!("\n=== Load Test Results ===");
println!("Total Requests: {}", self.total_requests);
println!("Successful: {}", self.successful_requests);
println!("Failed: {}", self.failed_requests);
println!(
"Success Rate: {:.2}%",
(self.successful_requests as f64 / self.total_requests as f64) * 100.0
);
println!("Duration: {:.2}s", self.total_duration.as_secs_f64());
println!("Requests/sec: {:.2}", self.requests_per_second);
println!(
"Avg Response Time: {:.2}ms",
self.avg_response_time.as_millis()
);
println!(
"Min Response Time: {:.2}ms",
self.min_response_time.as_millis()
);
println!(
"Max Response Time: {:.2}ms",
self.max_response_time.as_millis()
);
println!(
"95th Percentile: {:.2}ms",
self.p95_response_time.as_millis()
);
println!(
"99th Percentile: {:.2}ms",
self.p99_response_time.as_millis()
);
}
}
pub async fn run_load_test(proxy_url: &str, path: &str, config: LoadTestConfig) -> LoadTestResults {
let client = reqwest::Client::builder()
.timeout(config.request_timeout)
.build()
.expect("Failed to create HTTP client");
let semaphore = Arc::new(Semaphore::new(config.concurrent_requests));
let mut tasks = Vec::new();
let mut response_times = Vec::new();
let start_time = Instant::now();
let url = format!("{proxy_url}{path}");
for i in 0..config.total_requests {
let client = client.clone();
let url = url.clone();
let semaphore = semaphore.clone();
let task = tokio::spawn(async move {
let _permit = semaphore.acquire().await.unwrap();
let request_start = Instant::now();
let result = client.get(&url).send().await;
let request_duration = request_start.elapsed();
match result {
Ok(response) => {
let success = response.status().is_success();
(success, request_duration, i)
}
Err(_) => (false, request_duration, i),
}
});
tasks.push(task);
if i % 100 == 0 && i > 0 {
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
let mut successful_requests = 0;
let mut failed_requests = 0;
for task in tasks {
match task.await {
Ok((success, duration, _)) => {
response_times.push(duration);
if success {
successful_requests += 1;
} else {
failed_requests += 1;
}
}
Err(_) => {
failed_requests += 1;
}
}
}
let total_duration = start_time.elapsed();
response_times.sort();
let avg_response_time = Duration::from_nanos(
(response_times.iter().map(|d| d.as_nanos()).sum::<u128>() / response_times.len() as u128)
as u64,
);
let min_response_time = response_times.first().copied().unwrap_or_default();
let max_response_time = response_times.last().copied().unwrap_or_default();
let p95_index = (response_times.len() as f64 * 0.95) as usize;
let p99_index = (response_times.len() as f64 * 0.99) as usize;
let p95_response_time = response_times.get(p95_index).copied().unwrap_or_default();
let p99_response_time = response_times.get(p99_index).copied().unwrap_or_default();
let requests_per_second = config.total_requests as f64 / total_duration.as_secs_f64();
LoadTestResults {
total_requests: config.total_requests,
successful_requests,
failed_requests,
total_duration,
requests_per_second,
avg_response_time,
min_response_time,
max_response_time,
p95_response_time,
p99_response_time,
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_basic_load() {
init_test_logging();
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/test"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(json!({"message": "Hello, World!"}))
.insert_header("content-type", "application/json"),
)
.mount(&mock_server)
.await;
let config = TestConfigProvider::new("load_test")
.with_value("server.port", 8080)
.with_routes(vec![
TestRoute::new(&mock_server.uri()).with_path("/api/test"),
]);
let foxy = Foxy::loader()
.with_provider(config)
.build()
.await
.expect("Failed to build Foxy instance");
let server_handle = tokio::spawn(async move { foxy.start().await });
tokio::time::sleep(Duration::from_millis(200)).await;
let load_config = LoadTestConfig {
concurrent_requests: 5,
total_requests: 100,
duration: Duration::from_secs(10),
request_timeout: Duration::from_secs(2),
};
let results = run_load_test("http://127.0.0.1:8080", "/api/test", load_config).await;
results.print_summary();
assert!(
results.successful_requests > 0,
"Should have some successful requests"
);
assert!(
results.requests_per_second > 0.0,
"Should have positive RPS"
);
assert!(
results.avg_response_time < Duration::from_secs(1),
"Average response time should be reasonable"
);
let success_rate = results.successful_requests as f64 / results.total_requests as f64;
assert!(
success_rate > 0.9,
"Success rate should be > 90%, got {:.2}%",
success_rate * 100.0
);
server_handle.abort();
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_high_concurrency_load() {
init_test_logging();
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/concurrent"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(json!({"timestamp": "2024-01-01T00:00:00Z"}))
.insert_header("content-type", "application/json"),
)
.mount(&mock_server)
.await;
let config = TestConfigProvider::new("concurrent_test")
.with_value("server.port", 8080)
.with_routes(vec![
TestRoute::new(&mock_server.uri()).with_path("/api/concurrent"),
]);
let foxy = Foxy::loader()
.with_provider(config)
.build()
.await
.expect("Failed to build Foxy instance");
let server_handle = tokio::spawn(async move { foxy.start().await });
tokio::time::sleep(Duration::from_millis(200)).await;
let load_config = LoadTestConfig {
concurrent_requests: 50,
total_requests: 500,
duration: Duration::from_secs(15),
request_timeout: Duration::from_secs(3),
};
let results = run_load_test("http://127.0.0.1:8080", "/api/concurrent", load_config).await;
results.print_summary();
assert!(
results.successful_requests > 0,
"Should handle concurrent requests"
);
assert!(
results.requests_per_second > 10.0,
"Should maintain reasonable throughput"
);
let success_rate = results.successful_requests as f64 / results.total_requests as f64;
assert!(
success_rate > 0.8,
"Success rate should be > 80% under high load, got {:.2}%",
success_rate * 100.0
);
server_handle.abort();
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_sustained_load() {
init_test_logging();
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/sustained"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(json!({"data": "sustained load test"}))
.insert_header("content-type", "application/json"),
)
.mount(&mock_server)
.await;
let config = TestConfigProvider::new("sustained_test")
.with_value("server.port", 8080)
.with_routes(vec![
TestRoute::new(&mock_server.uri()).with_path("/api/sustained"),
]);
let foxy = Foxy::loader()
.with_provider(config)
.build()
.await
.expect("Failed to build Foxy instance");
let server_handle = tokio::spawn(async move { foxy.start().await });
tokio::time::sleep(Duration::from_millis(200)).await;
let load_config = LoadTestConfig {
concurrent_requests: 20,
total_requests: 1000,
duration: Duration::from_secs(30),
request_timeout: Duration::from_secs(5),
};
let results = run_load_test("http://127.0.0.1:8080", "/api/sustained", load_config).await;
results.print_summary();
assert!(
results.successful_requests > 0,
"Should handle sustained load"
);
assert!(
results.requests_per_second > 5.0,
"Should maintain throughput over time"
);
assert!(
results.p95_response_time < Duration::from_secs(2),
"95th percentile should be reasonable: {:?}",
results.p95_response_time
);
server_handle.abort();
}