new(all): Initial import.

This commit is contained in:
Rodrigo Rodriguez 2024-12-25 16:29:12 -03:00
parent 2ba7ba9017
commit cbc762c512
2 changed files with 133 additions and 136 deletions

View file

@ -29,12 +29,13 @@ impl IntegrationTest {
// Start Redis // Start Redis
let redis = docker.run(testcontainers::images::redis::Redis::default()); let redis = docker.run(testcontainers::images::redis::Redis::default());
// Start Kafka
let kafka = docker.run(testcontainers::images::kafka::Kafka::default()); let kafka = docker.run(testcontainers::images::kafka::Kafka::default());
// Temporary placeholder for db_pool
let db_pool = unimplemented!("Database pool needs to be implemented");
Self { Self {
docker, docker,
db_pool: todo!(), db_pool,
} }}
}
} }

View file

@ -1,141 +1,137 @@
use goose::goose::TransactionError; use rand::{distributions::Alphanumeric, Rng};
use std::time::Duration;
use goose::prelude::*; /// Generates a random alphanumeric string of the specified length
///
fn get_default_name() -> &'static str { /// # Arguments
"default" /// * `length` - The desired length of the random string
///
/// # Returns
/// A String containing random alphanumeric characters
#[must_use]
pub fn generate_random_string(length: usize) -> String {
rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(length)
.map(char::from)
.collect()
} }
pub struct LoadTestConfig { /// Generates a vector of random bytes for testing purposes
pub users: usize, ///
pub ramp_up: usize, /// # Arguments
pub port: u16, /// * `size` - The number of random bytes to generate
///
/// # Returns
/// A Vec<u8> containing random bytes
#[must_use]
pub fn generate_test_data(size: usize) -> Vec<u8> {
let mut rng = rand::thread_rng();
(0..size).map(|_| rng.gen::<u8>()).collect()
} }
pub struct LoadTest { /// Executes an operation with exponential backoff retry strategy
config: LoadTestConfig, ///
} /// # Arguments
/// * `operation` - The async operation to execute
/// * `max_retries` - Maximum number of retry attempts
/// * `initial_delay` - Initial delay duration between retries
///
/// # Returns
/// Result containing the operation output or an error
///
/// # Errors
/// Returns the last error encountered after all retries are exhausted
pub async fn exponential_backoff<F, Fut, T>(
mut operation: F,
max_retries: u32,
initial_delay: Duration,
) -> anyhow::Result<T>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = anyhow::Result<T>>,
{
let mut retries = 0;
let mut delay = initial_delay;
impl LoadTest { loop {
pub fn new(config: LoadTestConfig) -> Self { match operation().await {
Self { config } Ok(value) => return Ok(value),
} Err(error) => {
if retries >= max_retries {
pub fn run(&self) -> Result<(), Box<dyn std::error::Error>> { return Err(anyhow::anyhow!("Operation failed after {} retries: {}", max_retries, error));
let mut goose = GooseAttack::initialize()?; }
tokio::time::sleep(delay).await;
goose delay = delay.saturating_mul(2); // Prevent overflow
.set_default(GooseDefault::Host, &format!("http://localhost:{}", self.config.port).as_str())? retries += 1;
.set_users(self.config.users)? }
.set_startup_time(self.config.ramp_up)?;
Ok(())
}
}
fn auth_scenario() -> Scenario {
scenario!("Authentication")
.register_transaction(transaction!(login))
.register_transaction(transaction!(logout))
}
async fn login(user: &mut GooseUser) -> TransactionResult {
let payload = serde_json::json!({
"email": "test@example.com",
"password": "password123"
});
let _response = user
.post_json("/auth/login", &payload)
.await?
.response
.map_err(|e| Box::new(TransactionError::RequestError(e.to_string())))?;
Ok(())
}
async fn logout(user: &mut GooseUser) -> TransactionResult {
let _response = user
.post("/auth/logout")
.await?
.response
.map_err(|e| Box::new(TransactionError::RequestError(e.to_string())))?;
Ok(())
}
fn api_scenario() -> Scenario {
scenario!("API")
.register_transaction(transaction!(create_instance))
.register_transaction(transaction!(list_instances))
}
async fn create_instance(user: &mut GooseUser) -> TransactionResult {
let payload = serde_json::json!({
"name": "test-instance",
"config": {
"memory": "512Mi",
"cpu": "0.5"
} }
});
let _response = user
.post_json("/api/instances", &payload)
.await?
.response
.map_err(|e| Box::new(TransactionError::RequestFailed(e.to_string())))?;
Ok(())
}
async fn list_instances(user: &mut GooseUser) -> TransactionResult {
let _response = user
.get("/api/instances")
.await?
.response
.map_err(|e| Box::new(TransactionError::RequestError(e.to_string())))?;
Ok(())
}
fn webrtc_scenario() -> Scenario {
scenario!("WebRTC")
.register_transaction(transaction!(join_room))
.register_transaction(transaction!(send_message))
}
async fn join_room(user: &mut GooseUser) -> TransactionResult {
let payload = serde_json::json!({
"room_id": "test-room",
"user_id": "test-user"
});
let _response = user
.post_json("/webrtc/rooms/join", &payload)
.await?
.response
.map_err(|e| Box::new(TransactionError::RequestError(e.to_string())))?;
Ok(())
}
async fn send_message(user: &mut GooseUser) -> TransactionResult {
let payload = serde_json::json!({
"room_id": "test-room",
"message": "test message"
});
let _response = user
.post_json("/webrtc/messages", &payload)
.await?
.response
.map_err(|e| Box::new(TransactionError::RequestError(e.to_string())))?;
Ok(())
}
impl From<reqwest::Error> for TransactionError {
fn from(error: reqwest::Error) -> Self {
TransactionError::RequestError(error.to_string())
} }
} }
/// Formats a Duration into a human-readable string in HH:MM:SS format
///
/// # Arguments
/// * `duration` - The Duration to format
///
/// # Returns
/// A String in the format "HH:MM:SS"
#[must_use]
pub fn format_duration(duration: Duration) -> String {
let total_seconds = duration.as_secs();
let hours = total_seconds / 3600;
let minutes = (total_seconds % 3600) / 60;
let seconds = total_seconds % 60;
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_random_string() {
let length = 10;
let result = generate_random_string(length);
assert_eq!(result.len(), length);
}
#[test]
fn test_generate_test_data() {
let size = 100;
let result = generate_test_data(size);
assert_eq!(result.len(), size);
}
#[test]
fn test_format_duration() {
let duration = Duration::from_secs(3661); // 1 hour, 1 minute, 1 second
assert_eq!(format_duration(duration), "01:01:01");
}
#[tokio::test]
async fn test_exponential_backoff() {
// Use interior mutability with RefCell to allow mutation in the closure
use std::cell::RefCell;
let counter = RefCell::new(0);
let operation = || async {
*counter.borrow_mut() += 1;
if *counter.borrow() < 3 {
Err(anyhow::anyhow!("Test error"))
} else {
Ok(*counter.borrow())
}
};
let result = exponential_backoff(
operation,
5,
Duration::from_millis(1),
).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 3);
}
}