1use crate::error::{CleanroomError, Result};
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8use std::time::Duration;
9
10#[derive(Debug, Deserialize, Serialize, Clone)]
12pub struct CleanroomConfig {
13 pub project: ProjectConfig,
15 pub cli: CliConfig,
17 pub containers: ContainerConfig,
19 pub services: ServiceDefaultsConfig,
21 pub observability: ObservabilityConfig,
23 pub plugins: PluginConfig,
25 pub performance: PerformanceConfig,
27 pub test_execution: TestExecutionConfig,
29 pub reporting: ReportingConfig,
31 pub security: SecurityConfig,
33}
34
35#[derive(Debug, Deserialize, Serialize, Clone)]
37pub struct ProjectConfig {
38 pub name: String,
40 pub version: Option<String>,
42 pub description: Option<String>,
44}
45
46#[derive(Debug, Deserialize, Serialize, Clone)]
48pub struct CliConfig {
49 pub parallel: bool,
51 pub jobs: usize,
53 pub output_format: String,
55 pub fail_fast: bool,
57 pub watch: bool,
59 pub interactive: bool,
61}
62
63#[derive(Debug, Deserialize, Serialize, Clone)]
65pub struct ContainerConfig {
66 pub reuse_enabled: bool,
68 pub default_image: String,
70 pub pull_policy: String,
72 pub cleanup_policy: String,
74 pub max_containers: usize,
76 #[serde(deserialize_with = "super::deserializers::deserialize_duration")]
78 pub startup_timeout: Duration,
79}
80
81#[derive(Debug, Deserialize, Serialize, Clone)]
83pub struct ServiceDefaultsConfig {
84 #[serde(deserialize_with = "super::deserializers::deserialize_duration")]
86 pub default_timeout: Duration,
87 #[serde(deserialize_with = "super::deserializers::deserialize_duration")]
89 pub health_check_interval: Duration,
90 #[serde(deserialize_with = "super::deserializers::deserialize_duration")]
92 pub health_check_timeout: Duration,
93 pub max_retries: u32,
95}
96
97#[derive(Debug, Deserialize, Serialize, Clone)]
99pub struct ObservabilityConfig {
100 pub enable_tracing: bool,
102 pub enable_metrics: bool,
104 pub enable_logging: bool,
106 pub log_level: String,
108 pub metrics_port: u16,
110 pub traces_endpoint: Option<String>,
112}
113
114#[derive(Debug, Deserialize, Serialize, Clone)]
116pub struct PluginConfig {
117 pub auto_discover: bool,
119 pub plugin_dir: String,
121 pub enabled_plugins: Vec<String>,
123}
124
125#[derive(Debug, Deserialize, Serialize, Clone)]
127pub struct PerformanceConfig {
128 pub container_pool_size: usize,
130 pub lazy_initialization: bool,
132 pub cache_compiled_tests: bool,
134 pub parallel_step_execution: bool,
136}
137
138#[derive(Debug, Deserialize, Serialize, Clone)]
140pub struct TestExecutionConfig {
141 #[serde(deserialize_with = "super::deserializers::deserialize_duration")]
143 pub default_timeout: Duration,
144 #[serde(deserialize_with = "super::deserializers::deserialize_duration")]
146 pub step_timeout: Duration,
147 pub retry_on_failure: bool,
149 pub retry_count: u32,
151 pub test_dir: String,
153}
154
155#[derive(Debug, Deserialize, Serialize, Clone)]
157pub struct ReportingConfig {
158 pub generate_html: bool,
160 pub generate_junit: bool,
162 pub report_dir: String,
164 pub include_timestamps: bool,
166 pub include_logs: bool,
168}
169
170#[derive(Debug, Deserialize, Serialize, Clone)]
172pub struct SecurityConfig {
173 pub hermetic_isolation: bool,
175 pub network_isolation: bool,
177 pub file_system_isolation: bool,
179 pub security_level: String,
181}
182
183impl Default for CleanroomConfig {
184 fn default() -> Self {
185 Self {
186 project: ProjectConfig {
187 name: "cleanroom-project".to_string(),
188 version: Some("0.1.0".to_string()),
189 description: Some("Cleanroom integration tests".to_string()),
190 },
191 cli: CliConfig {
192 parallel: false,
193 jobs: 4,
194 output_format: "human".to_string(),
195 fail_fast: false,
196 watch: false,
197 interactive: false,
198 },
199 containers: ContainerConfig {
200 reuse_enabled: true,
201 default_image: "alpine:latest".to_string(),
202 pull_policy: "if-not-present".to_string(),
203 cleanup_policy: "on-success".to_string(),
204 max_containers: 10,
205 startup_timeout: Duration::from_secs(60),
206 },
207 services: ServiceDefaultsConfig {
208 default_timeout: Duration::from_secs(30),
209 health_check_interval: Duration::from_secs(5),
210 health_check_timeout: Duration::from_secs(10),
211 max_retries: 3,
212 },
213 observability: ObservabilityConfig {
214 enable_tracing: true,
215 enable_metrics: true,
216 enable_logging: true,
217 log_level: "info".to_string(),
218 metrics_port: 9090,
219 traces_endpoint: Some("http://localhost:4317".to_string()),
220 },
221 plugins: PluginConfig {
222 auto_discover: true,
223 plugin_dir: "./plugins".to_string(),
224 enabled_plugins: vec![
225 "surrealdb".to_string(),
226 "postgres".to_string(),
227 "redis".to_string(),
228 ],
229 },
230 performance: PerformanceConfig {
231 container_pool_size: 5,
232 lazy_initialization: true,
233 cache_compiled_tests: true,
234 parallel_step_execution: false,
235 },
236 test_execution: TestExecutionConfig {
237 default_timeout: Duration::from_secs(300),
238 step_timeout: Duration::from_secs(60),
239 retry_on_failure: false,
240 retry_count: 3,
241 test_dir: "./tests".to_string(),
242 },
243 reporting: ReportingConfig {
244 generate_html: true,
245 generate_junit: false,
246 report_dir: "./reports".to_string(),
247 include_timestamps: true,
248 include_logs: true,
249 },
250 security: SecurityConfig {
251 hermetic_isolation: true,
252 network_isolation: true,
253 file_system_isolation: true,
254 security_level: "medium".to_string(),
255 },
256 }
257 }
258}
259
260impl CleanroomConfig {
261 pub fn validate(&self) -> Result<()> {
263 if self.project.name.trim().is_empty() {
265 return Err(CleanroomError::validation_error(
266 "Project name cannot be empty",
267 ));
268 }
269
270 if self.cli.jobs == 0 {
272 return Err(CleanroomError::validation_error(
273 "CLI jobs must be greater than 0",
274 ));
275 }
276
277 if self.containers.max_containers == 0 {
279 return Err(CleanroomError::validation_error(
280 "Max containers must be greater than 0",
281 ));
282 }
283
284 if self.containers.startup_timeout.as_secs() == 0 {
285 return Err(CleanroomError::validation_error(
286 "Container startup timeout must be greater than 0",
287 ));
288 }
289
290 if self.observability.metrics_port == 0 {
292 return Err(CleanroomError::validation_error(
293 "Metrics port must be greater than 0",
294 ));
295 }
296
297 match self.security.security_level.to_lowercase().as_str() {
299 "low" | "medium" | "high" => {}
300 _ => {
301 return Err(CleanroomError::validation_error(
302 "Security level must be 'low', 'medium', or 'high'",
303 ))
304 }
305 }
306
307 match self.observability.log_level.to_lowercase().as_str() {
309 "debug" | "info" | "warn" | "error" => {}
310 _ => {
311 return Err(CleanroomError::validation_error(
312 "Log level must be 'debug', 'info', 'warn', or 'error'",
313 ))
314 }
315 }
316
317 Ok(())
318 }
319}
320
321pub fn load_cleanroom_config_from_file<P: AsRef<Path>>(path: P) -> Result<CleanroomConfig> {
323 let path = path.as_ref();
324 let content = std::fs::read_to_string(path).map_err(|e| {
325 CleanroomError::config_error(format!("Failed to read cleanroom.toml: {}", e))
326 })?;
327
328 let config: CleanroomConfig = toml::from_str(&content).map_err(|e| {
329 CleanroomError::config_error(format!("Invalid cleanroom.toml format: {}", e))
330 })?;
331
332 config.validate()?;
333 Ok(config)
334}
335
336fn load_cleanroom_config_from_user_dir() -> Result<CleanroomConfig> {
338 let user_config_dir = std::env::var("HOME")
339 .map(|home| {
340 Path::new(&home)
341 .join(".config")
342 .join("cleanroom")
343 .join("cleanroom.toml")
344 })
345 .unwrap_or_else(|_| Path::new("~/.config/cleanroom/cleanroom.toml").to_path_buf());
346
347 if user_config_dir.exists() {
348 load_cleanroom_config_from_file(user_config_dir)
349 } else {
350 Ok(CleanroomConfig::default())
351 }
352}
353
354fn apply_env_overrides(mut config: CleanroomConfig) -> Result<CleanroomConfig> {
356 if let Ok(parallel) = std::env::var("CLEANROOM_CLI_PARALLEL") {
358 config.cli.parallel = parallel.parse::<bool>().unwrap_or(config.cli.parallel);
359 }
360 if let Ok(jobs) = std::env::var("CLEANROOM_CLI_JOBS") {
361 config.cli.jobs = jobs.parse::<usize>().unwrap_or(config.cli.jobs);
362 }
363 if let Ok(format) = std::env::var("CLEANROOM_CLI_OUTPUT_FORMAT") {
364 config.cli.output_format = format;
365 }
366 if let Ok(fail_fast) = std::env::var("CLEANROOM_CLI_FAIL_FAST") {
367 config.cli.fail_fast = fail_fast.parse::<bool>().unwrap_or(config.cli.fail_fast);
368 }
369 if let Ok(watch) = std::env::var("CLEANROOM_CLI_WATCH") {
370 config.cli.watch = watch.parse::<bool>().unwrap_or(config.cli.watch);
371 }
372 if let Ok(interactive) = std::env::var("CLEANROOM_CLI_INTERACTIVE") {
373 config.cli.interactive = interactive
374 .parse::<bool>()
375 .unwrap_or(config.cli.interactive);
376 }
377
378 if let Ok(reuse) = std::env::var("CLEANROOM_CONTAINERS_REUSE_ENABLED") {
380 config.containers.reuse_enabled = reuse
381 .parse::<bool>()
382 .unwrap_or(config.containers.reuse_enabled);
383 }
384 if let Ok(default_image) = std::env::var("CLEANROOM_CONTAINERS_DEFAULT_IMAGE") {
385 config.containers.default_image = default_image;
386 }
387 if let Ok(max_containers) = std::env::var("CLEANROOM_CONTAINERS_MAX_CONTAINERS") {
388 config.containers.max_containers = max_containers
389 .parse::<usize>()
390 .unwrap_or(config.containers.max_containers);
391 }
392
393 if let Ok(tracing) = std::env::var("CLEANROOM_OBSERVABILITY_ENABLE_TRACING") {
395 config.observability.enable_tracing = tracing
396 .parse::<bool>()
397 .unwrap_or(config.observability.enable_tracing);
398 }
399 if let Ok(metrics) = std::env::var("CLEANROOM_OBSERVABILITY_ENABLE_METRICS") {
400 config.observability.enable_metrics = metrics
401 .parse::<bool>()
402 .unwrap_or(config.observability.enable_metrics);
403 }
404 if let Ok(logging) = std::env::var("CLEANROOM_OBSERVABILITY_ENABLE_LOGGING") {
405 config.observability.enable_logging = logging
406 .parse::<bool>()
407 .unwrap_or(config.observability.enable_logging);
408 }
409 if let Ok(log_level) = std::env::var("CLEANROOM_OBSERVABILITY_LOG_LEVEL") {
410 config.observability.log_level = log_level;
411 }
412
413 if let Ok(hermetic) = std::env::var("CLEANROOM_SECURITY_HERMETIC_ISOLATION") {
415 config.security.hermetic_isolation = hermetic
416 .parse::<bool>()
417 .unwrap_or(config.security.hermetic_isolation);
418 }
419 if let Ok(network) = std::env::var("CLEANROOM_SECURITY_NETWORK_ISOLATION") {
420 config.security.network_isolation = network
421 .parse::<bool>()
422 .unwrap_or(config.security.network_isolation);
423 }
424 if let Ok(filesystem) = std::env::var("CLEANROOM_SECURITY_FILESYSTEM_ISOLATION") {
425 config.security.file_system_isolation = filesystem
426 .parse::<bool>()
427 .unwrap_or(config.security.file_system_isolation);
428 }
429 if let Ok(security_level) = std::env::var("CLEANROOM_SECURITY_LEVEL") {
430 config.security.security_level = security_level;
431 }
432
433 Ok(config)
434}
435
436fn merge_configs(mut base: CleanroomConfig, override_config: CleanroomConfig) -> CleanroomConfig {
438 if !override_config.project.name.trim().is_empty() {
440 base.project.name = override_config.project.name;
441 }
442 if let Some(version) = override_config.project.version {
443 base.project.version = Some(version);
444 }
445 if let Some(description) = override_config.project.description {
446 base.project.description = Some(description);
447 }
448
449 base.cli = override_config.cli;
451 base.containers = override_config.containers;
452 base.services = override_config.services;
453 base.observability = override_config.observability;
454 base.plugins = override_config.plugins;
455 base.performance = override_config.performance;
456 base.test_execution = override_config.test_execution;
457 base.reporting = override_config.reporting;
458 base.security = override_config.security;
459
460 base
461}
462
463pub fn load_cleanroom_config() -> Result<CleanroomConfig> {
465 let mut config = CleanroomConfig::default();
466
467 if let Ok(user_config) = load_cleanroom_config_from_user_dir() {
469 config = merge_configs(config, user_config);
470 }
471
472 if let Ok(project_config) = load_cleanroom_config_from_file("cleanroom.toml") {
474 tracing::debug!(
475 "Loaded project cleanroom.toml with default_image: {}",
476 project_config.containers.default_image
477 );
478 config = merge_configs(config, project_config);
479 } else {
480 tracing::debug!("Failed to load project cleanroom.toml");
481 }
482
483 config = apply_env_overrides(config)?;
485
486 config.validate()?;
488
489 Ok(config)
490}