strest 0.1.10

Blazing-fast async HTTP load tester in Rust - lock-free design, real-time stats, distributed runs, and optional chart exports for high-load API testing.
Documentation
use async_trait::async_trait;

use crate::error::AppResult;

#[async_trait]
pub(crate) trait LocalRunPort<TAdapterArgs, TOutcome> {
    async fn run_local(&self, adapter_args: TAdapterArgs) -> AppResult<TOutcome>;
}

#[async_trait]
pub(crate) trait ReplayRunPort<TAdapterArgs> {
    async fn run_replay(&self, adapter_args: TAdapterArgs) -> AppResult<()>;
}

#[async_trait]
pub(crate) trait CleanupPort<TCleanupArgs> {
    async fn run_cleanup(&self, cleanup_args: TCleanupArgs) -> AppResult<()>;
}

#[async_trait]
pub(crate) trait ComparePort<TCompareArgs> {
    async fn run_compare(&self, compare_args: TCompareArgs) -> AppResult<()>;
}

pub(crate) trait ServicePort<TAdapterArgs> {
    fn handle_service_action(&self, adapter_args: TAdapterArgs) -> AppResult<()>;
}

pub(crate) async fn execute_local<TPort, TAdapterArgs, TOutcome>(
    adapter_args: TAdapterArgs,
    local_port: &TPort,
) -> AppResult<TOutcome>
where
    TPort: LocalRunPort<TAdapterArgs, TOutcome> + Sync,
{
    local_port.run_local(adapter_args).await
}

pub(crate) async fn execute_replay<TPort, TAdapterArgs>(
    adapter_args: TAdapterArgs,
    replay_port: &TPort,
) -> AppResult<()>
where
    TPort: ReplayRunPort<TAdapterArgs> + Sync,
{
    replay_port.run_replay(adapter_args).await
}

pub(crate) async fn execute_cleanup<TPort, TCleanupArgs>(
    cleanup_args: TCleanupArgs,
    cleanup_port: &TPort,
) -> AppResult<()>
where
    TPort: CleanupPort<TCleanupArgs> + Sync,
{
    cleanup_port.run_cleanup(cleanup_args).await
}

pub(crate) async fn execute_compare<TPort, TCompareArgs>(
    compare_args: TCompareArgs,
    compare_port: &TPort,
) -> AppResult<()>
where
    TPort: ComparePort<TCompareArgs> + Sync,
{
    compare_port.run_compare(compare_args).await
}

pub(crate) fn execute_service<TPort, TAdapterArgs>(
    adapter_args: TAdapterArgs,
    service_port: &TPort,
) -> AppResult<()>
where
    TPort: ServicePort<TAdapterArgs>,
{
    service_port.handle_service_action(adapter_args)
}

#[cfg(test)]
mod tests {
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::sync::{Arc, Mutex};

    use crate::error::AppResult;

    use super::{
        CleanupPort, ComparePort, LocalRunPort, ReplayRunPort, ServicePort, execute_cleanup,
        execute_compare, execute_local, execute_replay, execute_service,
    };

    struct FakeLocalPort {
        called: AtomicBool,
        seen: Arc<Mutex<Vec<String>>>,
    }

    #[async_trait::async_trait]
    impl LocalRunPort<String, usize> for FakeLocalPort {
        async fn run_local(&self, adapter_args: String) -> AppResult<usize> {
            self.called.store(true, Ordering::SeqCst);
            if let Ok(mut seen) = self.seen.lock() {
                seen.push(adapter_args.clone());
            }
            Ok(adapter_args.len())
        }
    }

    struct FakeReplayPort {
        called: AtomicBool,
        seen: Arc<Mutex<Vec<String>>>,
    }

    #[async_trait::async_trait]
    impl ReplayRunPort<String> for FakeReplayPort {
        async fn run_replay(&self, adapter_args: String) -> AppResult<()> {
            self.called.store(true, Ordering::SeqCst);
            if let Ok(mut seen) = self.seen.lock() {
                seen.push(adapter_args);
            }
            Ok(())
        }
    }

    struct FakeCleanupPort {
        called: AtomicBool,
        seen: Arc<Mutex<Vec<u64>>>,
    }

    #[async_trait::async_trait]
    impl CleanupPort<u64> for FakeCleanupPort {
        async fn run_cleanup(&self, cleanup_args: u64) -> AppResult<()> {
            self.called.store(true, Ordering::SeqCst);
            if let Ok(mut seen) = self.seen.lock() {
                seen.push(cleanup_args);
            }
            Ok(())
        }
    }

    struct FakeComparePort {
        called: AtomicBool,
        seen: Arc<Mutex<Vec<u64>>>,
    }

    #[async_trait::async_trait]
    impl ComparePort<u64> for FakeComparePort {
        async fn run_compare(&self, compare_args: u64) -> AppResult<()> {
            self.called.store(true, Ordering::SeqCst);
            if let Ok(mut seen) = self.seen.lock() {
                seen.push(compare_args);
            }
            Ok(())
        }
    }

    struct FakeServicePort {
        called: AtomicBool,
        seen: Arc<Mutex<Vec<String>>>,
    }

    impl ServicePort<String> for FakeServicePort {
        fn handle_service_action(&self, adapter_args: String) -> AppResult<()> {
            self.called.store(true, Ordering::SeqCst);
            if let Ok(mut seen) = self.seen.lock() {
                seen.push(adapter_args);
            }
            Ok(())
        }
    }

    #[tokio::test(flavor = "current_thread")]
    async fn execute_local_calls_port() -> AppResult<()> {
        let seen = Arc::new(Mutex::new(Vec::new()));
        let port = FakeLocalPort {
            called: AtomicBool::new(false),
            seen: seen.clone(),
        };

        let len = execute_local("local".to_owned(), &port).await?;

        if !port.called.load(Ordering::SeqCst) {
            return Err(crate::error::AppError::validation(
                "expected local port to be called",
            ));
        }
        if len != 5 {
            return Err(crate::error::AppError::validation(
                "expected local outcome to be returned",
            ));
        }
        if let Ok(seen) = seen.lock()
            && seen.as_slice() != ["local"]
        {
            return Err(crate::error::AppError::validation(
                "expected local args to be forwarded",
            ));
        }

        Ok(())
    }

    #[tokio::test(flavor = "current_thread")]
    async fn execute_replay_calls_port() -> AppResult<()> {
        let seen = Arc::new(Mutex::new(Vec::new()));
        let port = FakeReplayPort {
            called: AtomicBool::new(false),
            seen: seen.clone(),
        };

        execute_replay("replay".to_owned(), &port).await?;

        if !port.called.load(Ordering::SeqCst) {
            return Err(crate::error::AppError::validation(
                "expected replay port to be called",
            ));
        }
        if let Ok(seen) = seen.lock()
            && seen.as_slice() != ["replay"]
        {
            return Err(crate::error::AppError::validation(
                "expected replay args to be forwarded",
            ));
        }

        Ok(())
    }

    #[tokio::test(flavor = "current_thread")]
    async fn execute_cleanup_calls_port() -> AppResult<()> {
        let seen = Arc::new(Mutex::new(Vec::new()));
        let port = FakeCleanupPort {
            called: AtomicBool::new(false),
            seen: seen.clone(),
        };

        execute_cleanup(42, &port).await?;

        if !port.called.load(Ordering::SeqCst) {
            return Err(crate::error::AppError::validation(
                "expected cleanup port to be called",
            ));
        }
        if let Ok(seen) = seen.lock()
            && seen.as_slice() != [42]
        {
            return Err(crate::error::AppError::validation(
                "expected cleanup args to be forwarded",
            ));
        }

        Ok(())
    }

    #[tokio::test(flavor = "current_thread")]
    async fn execute_compare_calls_port() -> AppResult<()> {
        let seen = Arc::new(Mutex::new(Vec::new()));
        let port = FakeComparePort {
            called: AtomicBool::new(false),
            seen: seen.clone(),
        };

        execute_compare(77, &port).await?;

        if !port.called.load(Ordering::SeqCst) {
            return Err(crate::error::AppError::validation(
                "expected compare port to be called",
            ));
        }
        if let Ok(seen) = seen.lock()
            && seen.as_slice() != [77]
        {
            return Err(crate::error::AppError::validation(
                "expected compare args to be forwarded",
            ));
        }

        Ok(())
    }

    #[test]
    fn execute_service_calls_port() -> AppResult<()> {
        let seen = Arc::new(Mutex::new(Vec::new()));
        let port = FakeServicePort {
            called: AtomicBool::new(false),
            seen: seen.clone(),
        };

        execute_service("service".to_owned(), &port)?;

        if !port.called.load(Ordering::SeqCst) {
            return Err(crate::error::AppError::validation(
                "expected service port to be called",
            ));
        }
        if let Ok(seen) = seen.lock()
            && seen.as_slice() != ["service"]
        {
            return Err(crate::error::AppError::validation(
                "expected service args to be forwarded",
            ));
        }

        Ok(())
    }
}