use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use super::{IntegrationTestConfig, MockServiceRegistry, TestContext};
use crate::mcp::clients::MCPClient;
use crate::mcp::core::error::WorkflowError;
pub struct TestHarness {
context: Arc<TestContext>,
clients: Arc<Mutex<HashMap<String, Box<dyn MCPClient + Send + Sync>>>>,
cleanup_tasks: Arc<Mutex<Vec<Box<dyn CleanupTask>>>>,
}
#[async_trait::async_trait]
trait CleanupTask: Send + Sync {
async fn cleanup(&self) -> Result<(), String>;
}
impl TestHarness {
pub fn new(config: IntegrationTestConfig) -> Self {
Self {
context: Arc::new(TestContext::new(config)),
clients: Arc::new(Mutex::new(HashMap::new())),
cleanup_tasks: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn context(&self) -> Arc<TestContext> {
self.context.clone()
}
pub async fn register_client(&self, name: String, client: Box<dyn MCPClient + Send + Sync>) {
let mut clients = self.clients.lock().await;
clients.insert(name, client);
}
pub async fn get_client(&self, name: &str) -> Option<Box<dyn MCPClient + Send + Sync>> {
let clients = self.clients.lock().await;
None }
pub async fn run_test<F, Fut, T>(&self, test_fn: F) -> Result<T, String>
where
F: FnOnce(Arc<TestContext>) -> Fut,
Fut: std::future::Future<Output = Result<T, String>>,
{
self.setup().await?;
let result = test_fn(self.context.clone()).await;
self.cleanup().await?;
if let Err(errors) = self.context.registry.verify_all().await {
return Err(format!("Mock verification failed:\n{}", errors.join("\n")));
}
result
}
async fn setup(&self) -> Result<(), String> {
if self.context.config.log_level == "debug" {
self.context
.capture_log("Test harness initialized in debug mode".to_string())
.await;
}
let mut clients = self.clients.lock().await;
for (name, client) in clients.iter_mut() {
client
.connect()
.await
.map_err(|e| format!("Failed to connect client {}: {:?}", name, e))?;
client
.initialize("test-harness", "1.0.0")
.await
.map_err(|e| format!("Failed to initialize client {}: {:?}", name, e))?;
}
Ok(())
}
async fn cleanup(&self) -> Result<(), String> {
let mut errors = Vec::new();
let mut clients = self.clients.lock().await;
for (name, client) in clients.iter_mut() {
if let Err(e) = client.disconnect().await {
errors.push(format!("Failed to disconnect client {}: {:?}", name, e));
}
}
let cleanup_tasks = self.cleanup_tasks.lock().await;
for task in cleanup_tasks.iter() {
if let Err(e) = task.cleanup().await {
errors.push(e);
}
}
self.context.registry.reset_all().await;
if errors.is_empty() {
Ok(())
} else {
Err(errors.join("\n"))
}
}
pub async fn add_cleanup<F>(&self, cleanup_fn: F)
where
F: Fn() -> Result<(), String> + Send + Sync + 'static,
{
struct SimpleCleanup<F> {
cleanup_fn: F,
}
#[async_trait::async_trait]
impl<F> CleanupTask for SimpleCleanup<F>
where
F: Fn() -> Result<(), String> + Send + Sync,
{
async fn cleanup(&self) -> Result<(), String> {
(self.cleanup_fn)()
}
}
let mut tasks = self.cleanup_tasks.lock().await;
tasks.push(Box::new(SimpleCleanup { cleanup_fn }));
}
}
pub struct TestScenarioBuilder {
name: String,
config: IntegrationTestConfig,
setup_steps: Vec<Box<dyn SetupStep>>,
assertions: Vec<Box<dyn Assertion>>,
}
#[async_trait::async_trait]
trait SetupStep: Send + Sync {
async fn execute(&self, context: &TestContext) -> Result<(), String>;
}
#[async_trait::async_trait]
trait Assertion: Send + Sync {
async fn assert(&self, context: &TestContext) -> Result<(), String>;
}
impl TestScenarioBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
config: IntegrationTestConfig::default(),
setup_steps: Vec::new(),
assertions: Vec::new(),
}
}
pub fn with_config(mut self, config: IntegrationTestConfig) -> Self {
self.config = config;
self
}
pub fn with_setup<F>(mut self, setup_fn: F) -> Self
where
F: Fn(&TestContext) -> Result<(), String> + Send + Sync + 'static,
{
struct SimpleSetup<F> {
setup_fn: F,
}
#[async_trait::async_trait]
impl<F> SetupStep for SimpleSetup<F>
where
F: Fn(&TestContext) -> Result<(), String> + Send + Sync,
{
async fn execute(&self, context: &TestContext) -> Result<(), String> {
(self.setup_fn)(context)
}
}
self.setup_steps.push(Box::new(SimpleSetup { setup_fn }));
self
}
pub fn assert<F>(mut self, assertion_fn: F) -> Self
where
F: Fn(&TestContext) -> Result<(), String> + Send + Sync + 'static,
{
struct SimpleAssertion<F> {
assertion_fn: F,
}
#[async_trait::async_trait]
impl<F> Assertion for SimpleAssertion<F>
where
F: Fn(&TestContext) -> Result<(), String> + Send + Sync,
{
async fn assert(&self, context: &TestContext) -> Result<(), String> {
(self.assertion_fn)(context)
}
}
self.assertions
.push(Box::new(SimpleAssertion { assertion_fn }));
self
}
pub async fn run(self) -> Result<(), String> {
let harness = TestHarness::new(self.config);
harness
.run_test(|context| async move {
for step in &self.setup_steps {
step.execute(&context).await?;
}
for assertion in &self.assertions {
assertion.assert(&context).await?;
}
Ok(())
})
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mcp::testing::MockMCPClient;
#[tokio::test]
async fn test_harness_basic_flow() {
let config = IntegrationTestConfig::default();
let harness = TestHarness::new(config);
let client = Box::new(MockMCPClient::new());
harness
.register_client("test-client".to_string(), client)
.await;
let result = harness
.run_test(|context| async move {
context.capture_log("Test running".to_string()).await;
Ok(())
})
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_scenario_builder() {
let result = TestScenarioBuilder::new("test scenario")
.with_setup(|_context| {
Ok(())
})
.assert(|_context| {
Ok(())
})
.run()
.await;
assert!(result.is_ok());
}
}