use std::fs;
use std::time::Duration;
use serde_json::{Value, json};
use tracing::Level;
use crate::base::error::{TestError, TestsResult};
use crate::base::result::{TestModelResult, TestResult};
use crate::docker::compose::Compose;
use crate::engine::engine_builder::EngineBuilder;
use crate::engine::engine_dialog::Dialog;
use crate::engine::engine_env::EngineEnv;
use crate::engine::engine_events::EngineEvents;
use crate::engine::engine_report::EngineReport;
use crate::engine::engine_state::EngineState;
use crate::env::env_start::EnvStart;
use crate::env::env_stop::EnvStop;
use crate::mcp::client::McpClient;
use crate::mcp::runner::Runner;
use crate::ollama::client::OllamaClient;
pub struct EngineTests {
pub env: EngineEnv,
pub events: EngineEvents,
pub state: EngineState,
}
impl EngineTests {
pub fn builder() -> EngineBuilder {
EngineBuilder::new()
}
pub async fn init(&mut self) -> TestsResult<()> {
if self.state.initialized {
return Ok(());
}
self.state.initialized = true;
let tee = self.state.tee.clone();
tracing_subscriber::fmt()
.with_writer(move || tee.clone())
.with_max_level(Level::TRACE)
.try_init()
.ok();
if let Some(compose_file) = &self.env.compose_file {
self.state.compose = Some(
Compose::new(compose_file, self.state.tee.clone(), self.env.timeout)
.up()
.await?,
);
}
if let Some(on_start) = self.events.on_start.take() {
self.state.start_data = Some(
on_start(EnvStart {
home: self.state.home.path().to_path_buf(),
tee: self.state.tee.clone(),
})
.await?,
);
tokio::time::sleep(Duration::from_secs(2)).await;
}
let runner = Runner::new(&self.env.mcp_host, self.env.timeout);
runner.wait_healthy().await?;
self.state.runner = Some(runner);
Ok(())
}
pub async fn test(&self, query: &str) -> TestResult {
let mut models = Vec::new();
for model in &self.env.ollama_models {
let start = std::time::Instant::now();
EngineReport::trace_start(query, model);
let ollama = OllamaClient::new(&self.env.ollama_host);
if self.env.ollama_exclusive {
if let Err(e) = ollama.unload_except(model).await {
tracing::warn!("Failed to unload models: {}", e);
}
}
let mcp = McpClient::new(&self.env.mcp_host).await.unwrap();
let tools = mcp.list_tools().await.unwrap();
let tool_values: Vec<Value> = tools
.iter()
.map(|t| {
json!({
"type": "function",
"function": {
"name": t.name,
"description": t.description,
"parameters": t.input_schema
}
})
})
.collect();
let dialog = Dialog::new(ollama, mcp, model.clone(), tool_values, self.env.timeout);
let duration_ms = start.elapsed().as_millis() as u64;
let model_result = match dialog.run(query).await {
Ok(r) => {
EngineReport::trace_ok(
query,
model,
&r.tool,
&r.args,
&r.model_response,
&r.tool_response,
duration_ms,
);
TestModelResult {
model: model.clone(),
tool: Some(r.tool),
model_response: Some(r.model_response),
tool_response: Some(r.tool_response),
code: None,
}
}
Err(e) => {
let (tool, args, code) = match &e {
TestError::ToolCall(r) => (r.tool.as_deref(), r.args.as_deref(), r.code),
_ => (None, None, -1),
};
EngineReport::trace_fail(
query,
model,
tool,
args,
&e.to_string(),
code,
duration_ms,
);
TestModelResult {
model: model.clone(),
tool: tool.map(String::from),
model_response: None,
tool_response: None,
code: Some(code),
}
}
};
models.push(model_result);
}
let success = models.iter().all(|m| m.tool.is_some() && m.code.is_none());
TestResult { success, models }
}
pub fn shutdown(&mut self) {
let report =
EngineReport::from_log(&self.state.tee.path().to_string_lossy(), &self.env.mcp_host);
if let Some(on_stop) = self.events.on_stop.take() {
on_stop(EnvStop {
home: self.state.home.path().to_path_buf(),
log_file: self.state.tee.path().to_path_buf(),
duration: self.state.start_time.elapsed(),
data: self.state.start_data.take().unwrap_or(None),
report,
});
}
if let Some(runner) = &self.state.runner {
tracing::debug!("Checking if MCP server stopped...");
match runner.wait_dead() {
Ok(()) => tracing::debug!("MCP server stopped"),
Err(_) => tracing::warn!("MCP server may still be running"),
}
}
if let Some(mut compose) = self.state.compose.take() {
compose.down();
}
let _ = fs::remove_dir_all(self.state.home.path());
}
}
impl Drop for EngineTests {
fn drop(&mut self) {
self.shutdown();
}
}