From 9cf55ec6877b63bcc42a517a5a36187fbc3d8a44 Mon Sep 17 00:00:00 2001 From: continuist Date: Mon, 23 Jun 2025 22:32:46 -0400 Subject: [PATCH] Add performance tests to integration-tests crate --- .../src/api_postgres_tests.rs | 10 +- backend/crates/integration-tests/src/lib.rs | 4 +- .../src/performance_tests.rs | 529 ++++++++++++++++++ 3 files changed, 537 insertions(+), 6 deletions(-) create mode 100644 backend/crates/integration-tests/src/performance_tests.rs diff --git a/backend/crates/integration-tests/src/api_postgres_tests.rs b/backend/crates/integration-tests/src/api_postgres_tests.rs index 42e7a49..ffc500f 100644 --- a/backend/crates/integration-tests/src/api_postgres_tests.rs +++ b/backend/crates/integration-tests/src/api_postgres_tests.rs @@ -50,7 +50,7 @@ where } // Helper functions -async fn setup_test_db() -> PgPool { +pub async fn setup_test_db() -> PgPool { let database_url = env::var("DATABASE_URL") .unwrap_or_else(|_| "postgres://postgres:password@localhost:5432/sharenet_test".to_string()); let pool = PgPoolOptions::new() @@ -66,19 +66,19 @@ async fn setup_test_db() -> PgPool { pool } -async fn cleanup_test_data(pool: &PgPool) { +pub async fn cleanup_test_data(pool: &PgPool) { let mut tx = pool.begin().await.expect("Failed to begin transaction"); sqlx::query("DELETE FROM products").execute(&mut *tx).await.expect("Failed to delete products"); sqlx::query("DELETE FROM users").execute(&mut *tx).await.expect("Failed to delete users"); tx.commit().await.expect("Failed to commit cleanup transaction"); } -fn unique_test_data(prefix: &str) -> (String, String) { +pub fn unique_test_data(prefix: &str) -> (String, String) { let id = Uuid::new_v4().to_string()[..8].to_string(); (format!("{}_{}", prefix, id), format!("{}_test@example.com", prefix)) } -async fn create_test_app() -> Router { +pub async fn create_test_app() -> Router { let pool = setup_test_db().await; let user_repo = PostgresUserRepository::new(pool.clone()); let product_repo = PostgresProductRepository::new(pool.clone()); @@ -108,7 +108,7 @@ async fn create_test_app() -> Router { .with_state(state) } -async fn extract_json(response: axum::response::Response) -> T { +pub async fn extract_json(response: axum::response::Response) -> T { let bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); serde_json::from_slice(&bytes).unwrap() } diff --git a/backend/crates/integration-tests/src/lib.rs b/backend/crates/integration-tests/src/lib.rs index 65262cc..0e9545d 100644 --- a/backend/crates/integration-tests/src/lib.rs +++ b/backend/crates/integration-tests/src/lib.rs @@ -12,4 +12,6 @@ #[cfg(test)] pub mod api_postgres_tests; #[cfg(test)] -pub mod migration_tests; \ No newline at end of file +pub mod migration_tests; +#[cfg(test)] +pub mod performance_tests; \ No newline at end of file diff --git a/backend/crates/integration-tests/src/performance_tests.rs b/backend/crates/integration-tests/src/performance_tests.rs new file mode 100644 index 0000000..0146a84 --- /dev/null +++ b/backend/crates/integration-tests/src/performance_tests.rs @@ -0,0 +1,529 @@ +/* + * This file is part of Sharenet. + * + * Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. + * + * You may obtain a copy of the license at: + * https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * Copyright (c) 2024 Continuist + */ + +use axum::{ + body::Body, + http::{Request, StatusCode}, + Router, +}; +use std::time::{Duration, Instant}; +use tokio::time::sleep; +use serde_json::json; +use serial_test::serial; +use tower::ServiceExt; + +// Reuse AppState and helper functions from api_postgres_tests +use crate::api_postgres_tests::{create_test_app, unique_test_data}; + +/// Performance metrics structure +#[derive(Debug)] +struct PerformanceMetrics { + operation: String, + total_requests: usize, + successful_requests: usize, + failed_requests: usize, + total_duration: Duration, + avg_duration: Duration, + min_duration: Duration, + max_duration: Duration, + requests_per_second: f64, +} + +impl PerformanceMetrics { + fn new(operation: String, total_requests: usize, successful_requests: usize, failed_requests: usize, total_duration: Duration, durations: &[Duration]) -> Self { + let avg_duration = if !durations.is_empty() { + Duration::from_nanos(durations.iter().map(|d| d.as_nanos() as u64).sum::() / durations.len() as u64) + } else { + Duration::ZERO + }; + + let min_duration = durations.iter().min().copied().unwrap_or(Duration::ZERO); + let max_duration = durations.iter().max().copied().unwrap_or(Duration::ZERO); + let requests_per_second = if total_duration.as_secs_f64() > 0.0 { + successful_requests as f64 / total_duration.as_secs_f64() + } else { + 0.0 + }; + + Self { + operation, + total_requests, + successful_requests, + failed_requests, + total_duration, + avg_duration, + min_duration, + max_duration, + requests_per_second, + } + } + + fn print_report(&self) { + println!("\n=== Performance Report: {} ===", self.operation); + 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!("Total Duration: {:.2?}", self.total_duration); + println!("Average Duration: {:.2?}", self.avg_duration); + println!("Min Duration: {:.2?}", self.min_duration); + println!("Max Duration: {:.2?}", self.max_duration); + println!("Requests/Second: {:.2}", self.requests_per_second); + println!("====================================="); + } +} + +/// Run concurrent user creation test +async fn run_concurrent_user_creation(app: Router, num_concurrent: usize) -> PerformanceMetrics { + println!("Starting concurrent user creation test with {} concurrent requests...", num_concurrent); + + let start_time = Instant::now(); + let mut durations = Vec::new(); + let mut successful = 0; + let mut failed = 0; + + let handles: Vec<_> = (0..num_concurrent) + .map(|i| { + let app = app.clone(); + let (username, email) = unique_test_data(&format!("perf_user_{}", i)); + + tokio::spawn(async move { + let request_start = Instant::now(); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/users") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "username": username, + "email": email + }) + .to_string(), + )) + .unwrap(), + ) + .await; + + let duration = request_start.elapsed(); + + match response { + Ok(resp) if resp.status() == StatusCode::CREATED => { + (duration, true) + } + _ => (duration, false) + } + }) + }) + .collect(); + + for handle in handles { + match handle.await { + Ok((duration, success)) => { + durations.push(duration); + if success { + successful += 1; + } else { + failed += 1; + } + } + Err(_) => { + failed += 1; + } + } + } + + let total_duration = start_time.elapsed(); + + PerformanceMetrics::new( + format!("Concurrent User Creation ({} requests)", num_concurrent), + num_concurrent, + successful, + failed, + total_duration, + &durations, + ) +} + +/// Run concurrent product creation test +async fn run_concurrent_product_creation(app: Router, num_concurrent: usize) -> PerformanceMetrics { + println!("Starting concurrent product creation test with {} concurrent requests...", num_concurrent); + + let start_time = Instant::now(); + let mut durations = Vec::new(); + let mut successful = 0; + let mut failed = 0; + + let handles: Vec<_> = (0..num_concurrent) + .map(|i| { + let app = app.clone(); + let (name, _) = unique_test_data(&format!("perf_product_{}", i)); + + tokio::spawn(async move { + let request_start = Instant::now(); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/products") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "name": name, + "description": format!("Performance test product {}", i) + }) + .to_string(), + )) + .unwrap(), + ) + .await; + + let duration = request_start.elapsed(); + + match response { + Ok(resp) if resp.status() == StatusCode::CREATED => { + (duration, true) + } + _ => (duration, false) + } + }) + }) + .collect(); + + for handle in handles { + match handle.await { + Ok((duration, success)) => { + durations.push(duration); + if success { + successful += 1; + } else { + failed += 1; + } + } + Err(_) => { + failed += 1; + } + } + } + + let total_duration = start_time.elapsed(); + + PerformanceMetrics::new( + format!("Concurrent Product Creation ({} requests)", num_concurrent), + num_concurrent, + successful, + failed, + total_duration, + &durations, + ) +} + +/// Run mixed workload test (users and products) +async fn run_mixed_workload_test(app: Router, num_operations: usize) -> PerformanceMetrics { + println!("Starting mixed workload test with {} operations...", num_operations); + + let start_time = Instant::now(); + let mut durations = Vec::new(); + let mut successful = 0; + let mut failed = 0; + + let handles: Vec<_> = (0..num_operations) + .map(|i| { + let app = app.clone(); + + tokio::spawn(async move { + let request_start = Instant::now(); + let is_user_operation = i % 2 == 0; + + let response = if is_user_operation { + let (username, email) = unique_test_data(&format!("mixed_user_{}", i)); + app.clone() + .oneshot( + Request::builder() + .method("POST") + .uri("/users") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "username": username, + "email": email + }) + .to_string(), + )) + .unwrap(), + ) + .await + } else { + let (name, _) = unique_test_data(&format!("mixed_product_{}", i)); + app.oneshot( + Request::builder() + .method("POST") + .uri("/products") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "name": name, + "description": format!("Mixed workload product {}", i) + }) + .to_string(), + )) + .unwrap(), + ) + .await + }; + + let duration = request_start.elapsed(); + + match response { + Ok(resp) if resp.status() == StatusCode::CREATED => { + (duration, true) + } + _ => (duration, false) + } + }) + }) + .collect(); + + for handle in handles { + match handle.await { + Ok((duration, success)) => { + durations.push(duration); + if success { + successful += 1; + } else { + failed += 1; + } + } + Err(_) => { + failed += 1; + } + } + } + + let total_duration = start_time.elapsed(); + + PerformanceMetrics::new( + format!("Mixed Workload ({} operations)", num_operations), + num_operations, + successful, + failed, + total_duration, + &durations, + ) +} + +/// Run database connection pool stress test +async fn run_connection_pool_stress_test(app: Router, num_connections: usize) -> PerformanceMetrics { + println!("Starting connection pool stress test with {} concurrent connections...", num_connections); + + let start_time = Instant::now(); + let mut durations = Vec::new(); + let mut successful = 0; + let mut failed = 0; + + let handles: Vec<_> = (0..num_connections) + .map(|i| { + let app = app.clone(); + + tokio::spawn(async move { + let request_start = Instant::now(); + + // Make multiple requests to stress the connection pool + let mut success_count = 0; + for j in 0..5 { + let (username, email) = unique_test_data(&format!("pool_user_{}_{}", i, j)); + + let response = app + .clone() + .oneshot( + Request::builder() + .method("POST") + .uri("/users") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "username": username, + "email": email + }) + .to_string(), + )) + .unwrap(), + ) + .await; + + if response.unwrap().status() == StatusCode::CREATED { + success_count += 1; + } + + // Small delay to simulate real-world usage + sleep(Duration::from_millis(10)).await; + } + + let duration = request_start.elapsed(); + (duration, success_count == 5) + }) + }) + .collect(); + + for handle in handles { + match handle.await { + Ok((duration, success)) => { + durations.push(duration); + if success { + successful += 1; + } else { + failed += 1; + } + } + Err(_) => { + failed += 1; + } + } + } + + let total_duration = start_time.elapsed(); + + PerformanceMetrics::new( + format!("Connection Pool Stress ({} connections)", num_connections), + num_connections, + successful, + failed, + total_duration, + &durations, + ) +} + +#[tokio::test] +#[serial] +async fn test_performance_and_stress() { + println!("\nšŸš€ Starting Performance and Stress Tests"); + println!("========================================="); + + let app = create_test_app().await; + + // Test 1: Concurrent user creation + let user_metrics = run_concurrent_user_creation(app.clone(), 50).await; + user_metrics.print_report(); + + // Test 2: Concurrent product creation + let product_metrics = run_concurrent_product_creation(app.clone(), 50).await; + product_metrics.print_report(); + + // Test 3: Mixed workload + let mixed_metrics = run_mixed_workload_test(app.clone(), 100).await; + mixed_metrics.print_report(); + + // Test 4: Connection pool stress + let pool_metrics = run_connection_pool_stress_test(app.clone(), 20).await; + pool_metrics.print_report(); + + // Summary + println!("\nšŸ“Š Performance Test Summary"); + println!("============================"); + println!("All performance tests completed successfully!"); + println!("Check the individual reports above for detailed metrics."); + + // Assert reasonable performance (adjust thresholds as needed) + assert!(user_metrics.requests_per_second > 10.0, "User creation should handle at least 10 req/s"); + assert!(product_metrics.requests_per_second > 10.0, "Product creation should handle at least 10 req/s"); + assert!(mixed_metrics.requests_per_second > 8.0, "Mixed workload should handle at least 8 req/s"); + assert!(pool_metrics.successful_requests > 0, "Connection pool should handle some requests"); +} + +#[tokio::test] +#[serial] +async fn test_concurrent_user_creation() { + println!("\nšŸš€ Starting Concurrent User Creation Performance Test"); + println!("====================================================="); + + let app = create_test_app().await; + let num_concurrent = 50; + + println!("Testing with {} concurrent requests...", num_concurrent); + + let start_time = Instant::now(); + let mut durations = Vec::new(); + let mut successful = 0; + let mut failed = 0; + + let handles: Vec<_> = (0..num_concurrent) + .map(|i| { + let app = app.clone(); + let (username, email) = unique_test_data(&format!("perf_user_{}", i)); + + tokio::spawn(async move { + let request_start = Instant::now(); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/users") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "username": username, + "email": email + }) + .to_string(), + )) + .unwrap(), + ) + .await; + + let duration = request_start.elapsed(); + + match response { + Ok(resp) if resp.status() == StatusCode::CREATED => { + (duration, true) + } + _ => (duration, false) + } + }) + }) + .collect(); + + for handle in handles { + match handle.await { + Ok((duration, success)) => { + durations.push(duration); + if success { + successful += 1; + } else { + failed += 1; + } + } + Err(_) => { + failed += 1; + } + } + } + + let total_duration = start_time.elapsed(); + + let metrics = PerformanceMetrics::new( + format!("Concurrent User Creation ({} requests)", num_concurrent), + num_concurrent, + successful, + failed, + total_duration, + &durations, + ); + + metrics.print_report(); + + // Assert reasonable performance + assert!(metrics.requests_per_second > 10.0, "User creation should handle at least 10 req/s"); + assert!(metrics.successful_requests > 0, "Should have some successful requests"); +} \ No newline at end of file