use tempfile::TempDir;
use tracing::{info, warn};
use super::container::Services;
use crate::config::Config;
use crate::domain::environment::state::AnyEnvironmentState;
use crate::domain::Environment;
use crate::testing::e2e::LXD_OPENTOFU_SUBFOLDER;
#[derive(Debug, thiserror::Error)]
pub enum TestContextError {
#[error("Templates directory cannot be empty")]
EmptyTemplatesDirectory,
#[error("Templates directory cannot be empty or whitespace-only")]
WhitespaceOnlyTemplatesDirectory,
#[error("Failed to determine current directory (project root): {0}")]
CurrentDirectoryError(#[from] std::io::Error),
#[error("Failed to create temporary directory for test context SSH keys: {source}")]
TempDirectoryCreationError { source: std::io::Error },
#[error("Failed to setup SSH keys for test context: {source}")]
SshKeySetupError { source: anyhow::Error },
#[error("Failed to clean and prepare templates directory: {source}")]
ContextPreparationError { source: anyhow::Error },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TestContextType {
Container,
VirtualMachine,
}
pub struct TestContext {
pub config: Config,
pub services: Services,
pub environment: AnyEnvironmentState,
pub keep_env: bool,
temp_dir: Option<tempfile::TempDir>,
context_type: TestContextType,
}
impl TestContext {
fn new(
keep_env: bool,
environment: Environment,
context_type: TestContextType,
) -> Result<Self, TestContextError> {
let templates_dir = environment.templates_dir();
Self::validate_inputs(&templates_dir)?;
let project_root = Self::get_project_root()?;
let temp_dir = Self::create_temp_directory()?;
let config = Config::new(
environment.templates_dir().clone(),
project_root,
environment.build_dir().clone(),
);
let services = Services::new(
&config,
environment.ssh_credentials().clone(),
environment.instance_name().clone(),
environment.provider_config().clone(),
);
let env = Self {
config,
services,
environment: environment.into_any(), keep_env,
temp_dir: Some(temp_dir),
context_type,
};
Ok(env)
}
pub fn from_environment(
keep_env: bool,
environment: Environment,
context_type: TestContextType,
) -> Result<Self, TestContextError> {
Self::new(keep_env, environment, context_type)
}
pub fn init(self) -> Result<Self, TestContextError> {
Self::prepare_environment(&self.services)?;
self.persist_initial_environment_state()?;
self.log_environment_info();
Ok(self)
}
fn validate_inputs(templates_dir: &std::path::Path) -> Result<(), TestContextError> {
if templates_dir.as_os_str().is_empty() {
return Err(TestContextError::EmptyTemplatesDirectory);
}
if let Some(dir_str) = templates_dir.to_str() {
if dir_str.trim().is_empty() {
return Err(TestContextError::WhitespaceOnlyTemplatesDirectory);
}
}
Ok(())
}
fn get_project_root() -> Result<std::path::PathBuf, TestContextError> {
std::env::current_dir().map_err(TestContextError::CurrentDirectoryError)
}
fn create_temp_directory() -> Result<TempDir, TestContextError> {
TempDir::new().map_err(|e| TestContextError::TempDirectoryCreationError { source: e })
}
fn prepare_environment(services: &Services) -> Result<(), TestContextError> {
info!(
operation = "clean_templates",
"Cleaning templates directory to ensure fresh embedded templates"
);
services
.template_manager
.reset_templates_dir()
.map_err(|e| TestContextError::ContextPreparationError {
source: anyhow::anyhow!(e),
})?;
Ok(())
}
fn persist_initial_environment_state(&self) -> Result<(), TestContextError> {
let repository = self.create_repository();
info!(
environment = %self.environment.name(),
state = %self.environment.state_name(),
"Persisting initial environment state"
);
repository
.save(&self.environment)
.map_err(|e| TestContextError::ContextPreparationError {
source: anyhow::anyhow!("Failed to persist initial environment state: {e}"),
})
}
fn log_environment_info(&self) {
if self.keep_env && self.context_type == TestContextType::Container {
warn!(
environment_type = "container",
keep_env = true,
"keep_env flag is enabled but Container environments are automatically destroyed by testcontainers - the flag will be ignored"
);
}
if let Some(temp_path) = self.temp_dir_path() {
info!(
environment = "temporary_directory",
path = %temp_path.display(),
"Temporary directory created"
);
}
info!(
environment = "templates_directory",
path = %self.services.template_manager.templates_dir().display(),
"Templates directory configured"
);
if let Some(temp_path) = self.temp_dir_path() {
info!(
temp_dir_path = %temp_path.display(),
"Test context initialized with temporary directory"
);
}
}
#[must_use]
pub fn temp_dir_path(&self) -> Option<&std::path::Path> {
self.temp_dir.as_ref().map(tempfile::TempDir::path)
}
pub fn update_from_provisioned(
&mut self,
provisioned_env: crate::domain::Environment<crate::domain::environment::Provisioned>,
) {
self.environment = provisioned_env.into_any();
}
pub fn update_from_configured(
&mut self,
configured_env: crate::domain::Environment<crate::domain::environment::Configured>,
) {
self.environment = configured_env.into_any();
}
pub fn update_from_destroyed(
&mut self,
destroyed_env: crate::domain::Environment<crate::domain::environment::Destroyed>,
) {
self.environment = destroyed_env.into_any();
}
#[must_use]
pub fn create_repository(
&self,
) -> std::sync::Arc<dyn crate::domain::environment::repository::EnvironmentRepository> {
let base_data_dir = self.config.project_root.join("data");
self.services.file_repository_factory.create(base_data_dir)
}
}
impl std::fmt::Debug for TestContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TestContext")
.field("keep_env", &self.keep_env)
.field("templates_dir", &self.config.templates_dir)
.field("project_root", &self.config.project_root)
.field("build_dir", &self.config.build_dir)
.field("has_temp_dir", &self.temp_dir.is_some())
.finish_non_exhaustive() }
}
impl Drop for TestContext {
fn drop(&mut self) {
if !self.keep_env {
match self.context_type {
TestContextType::VirtualMachine => {
if matches!(self.environment, AnyEnvironmentState::Destroyed(_)) {
return;
}
let tofu_dir = self.config.build_dir.join(LXD_OPENTOFU_SUBFOLDER);
if let Err(e) = crate::adapters::tofu::emergency_destroy(&tofu_dir) {
eprintln!("Warning: Failed to cleanup OpenTofu resources during TestContext drop: {e}");
}
}
TestContextType::Container => {
}
}
}
}
}