new(all): Initial import.
This commit is contained in:
parent
2ba7ba9017
commit
cbc762c512
2 changed files with 133 additions and 136 deletions
|
@ -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,
|
||||||
}
|
}}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue