use std::collections::HashMap;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use tempfile::tempdir;
use tracing::Level;
use crate::EngineTests;
use crate::base::alias::{OnLog, OnRun, OnStart, OnStop};
use crate::base::error::TestError;
use crate::base::error::TestsResult;
use crate::base::tee_writer::TeeWriter;
use crate::engine::engine_env::EngineEnv;
use crate::engine::engine_events::EngineEvents;
use crate::engine::engine_state::EngineState;
use crate::env::env_log::EnvLog;
use crate::env::env_run::EnvRun;
use crate::env::env_start::EnvStart;
use crate::env::env_stop::EnvStop;
pub struct EngineBuilder {
compose_file: Option<PathBuf>,
mcp_host: String,
ollama_host: String,
ollama_models: Vec<String>,
ollama_exclusive: bool,
on_start: Option<OnStart>,
on_run: Option<OnRun>,
on_stop: Option<OnStop>,
on_log: Option<OnLog>,
log_level: Level,
timeout: Duration,
}
impl EngineBuilder {
pub fn new() -> Self {
Self {
compose_file: None,
mcp_host: String::new(),
ollama_host: String::new(),
ollama_models: Vec::new(),
ollama_exclusive: true,
on_start: None,
on_run: None,
on_stop: None,
on_log: None,
log_level: Level::ERROR,
timeout: Duration::from_secs(60),
}
}
pub fn compose_file(mut self, path: impl Into<PathBuf>) -> Self {
self.compose_file = Some(path.into());
self
}
pub fn mcp_host(mut self, host: impl Into<String>) -> Self {
self.mcp_host = host.into();
self
}
pub fn ollama_host(mut self, host: impl Into<String>) -> Self {
self.ollama_host = host.into();
self
}
pub fn ollama_models(mut self, models: &[&str]) -> Self {
self.ollama_models = models.iter().map(|s| s.to_string()).collect();
self
}
pub fn ollama_exclusive(mut self, yes: bool) -> Self {
self.ollama_exclusive = yes;
self
}
pub fn on_start<F, Fut>(mut self, f: F) -> Self
where
F: FnOnce(EnvStart) -> Fut + Send + 'static,
Fut: Future<Output = TestsResult<Option<HashMap<String, String>>>> + Send + 'static,
{
self.on_start = Some(Box::new(move |env| Box::pin(f(env))));
self
}
pub fn on_run<F, Fut>(mut self, f: F) -> Self
where
F: Fn(EnvRun) -> Fut + Send + Sync + 'static,
Fut: Future<Output = TestsResult<()>> + Send + 'static,
{
self.on_run = Some(Box::new(move |env| Box::pin(f(env))));
self
}
pub fn on_stop<F>(mut self, f: F) -> Self
where
F: FnOnce(EnvStop) + Send + 'static,
{
self.on_stop = Some(Box::new(f));
self
}
pub fn on_log<F>(mut self, f: F) -> Self
where
F: Fn(EnvLog) + Send + Sync + 'static,
{
self.on_log = Some(Box::new(f));
self
}
pub fn log_level(mut self, level: Level) -> Self {
self.log_level = level;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn build(self) -> TestsResult<EngineTests> {
if self.ollama_host.is_empty() {
return Err(TestError::Setup("ollama_host must not be empty".into()));
}
if self.ollama_models.is_empty() {
return Err(TestError::Setup("ollama_models must not be empty".into()));
}
if self.mcp_host.is_empty() {
return Err(TestError::Setup("mcp_host must not be empty".into()));
}
let home =
tempdir().map_err(|e| TestError::Setup(format!("Failed to create temp dir: {}", e)))?;
let log_path = home.path().join("vibe-tests.log");
let tee = TeeWriter::new(log_path, self.log_level, self.on_log)
.map_err(|e| TestError::Setup(format!("Failed to create TeeWriter: {}", e)))?;
Ok(EngineTests {
env: EngineEnv {
compose_file: self.compose_file,
mcp_host: self.mcp_host,
ollama_host: self.ollama_host,
ollama_models: self.ollama_models,
ollama_exclusive: self.ollama_exclusive,
log_level: self.log_level,
timeout: self.timeout,
},
events: EngineEvents {
on_start: self.on_start,
on_run: self.on_run,
on_stop: self.on_stop,
},
state: EngineState {
initialized: false,
home,
tee,
compose: None,
runner: None,
start_data: None,
start_time: Instant::now(),
},
})
}
}