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 pub startup_timeout: Duration,
78}
79
80#[derive(Debug, Deserialize, Serialize, Clone)]
82pub struct ServiceDefaultsConfig {
83 pub default_timeout: Duration,
85 pub health_check_interval: Duration,
87 pub health_check_timeout: Duration,
89 pub max_retries: u32,
91}
92
93#[derive(Debug, Deserialize, Serialize, Clone)]
95pub struct ObservabilityConfig {
96 pub enable_tracing: bool,
98 pub enable_metrics: bool,
100 pub enable_logging: bool,
102 pub log_level: String,
104 pub metrics_port: u16,
106 pub traces_endpoint: Option<String>,
108}
109
110#[derive(Debug, Deserialize, Serialize, Clone)]
112pub struct PluginConfig {
113 pub auto_discover: bool,
115 pub plugin_dir: String,
117 pub enabled_plugins: Vec<String>,
119}
120
121#[derive(Debug, Deserialize, Serialize, Clone)]
123pub struct PerformanceConfig {
124 pub container_pool_size: usize,
126 pub lazy_initialization: bool,
128 pub cache_compiled_tests: bool,
130 pub parallel_step_execution: bool,
132}
133
134#[derive(Debug, Deserialize, Serialize, Clone)]
136pub struct TestExecutionConfig {
137 pub default_timeout: Duration,
139 pub step_timeout: Duration,
141 pub retry_on_failure: bool,
143 pub retry_count: u32,
145 pub test_dir: String,
147}
148
149#[derive(Debug, Deserialize, Serialize, Clone)]
151pub struct ReportingConfig {
152 pub generate_html: bool,
154 pub generate_junit: bool,
156 pub report_dir: String,
158 pub include_timestamps: bool,
160 pub include_logs: bool,
162}
163
164#[derive(Debug, Deserialize, Serialize, Clone)]
166pub struct SecurityConfig {
167 pub hermetic_isolation: bool,
169 pub network_isolation: bool,
171 pub file_system_isolation: bool,
173 pub security_level: String,
175}
176
177impl Default for CleanroomConfig {
178 fn default() -> Self {
179 Self {
180 project: ProjectConfig {
181 name: "cleanroom-project".to_string(),
182 version: Some("0.1.0".to_string()),
183 description: Some("Cleanroom integration tests".to_string()),
184 },
185 cli: CliConfig {
186 parallel: false,
187 jobs: 4,
188 output_format: "human".to_string(),
189 fail_fast: false,
190 watch: false,
191 interactive: false,
192 },
193 containers: ContainerConfig {
194 reuse_enabled: true,
195 default_image: "alpine:latest".to_string(),
196 pull_policy: "if-not-present".to_string(),
197 cleanup_policy: "on-success".to_string(),
198 max_containers: 10,
199 startup_timeout: Duration::from_secs(60),
200 },
201 services: ServiceDefaultsConfig {
202 default_timeout: Duration::from_secs(30),
203 health_check_interval: Duration::from_secs(5),
204 health_check_timeout: Duration::from_secs(10),
205 max_retries: 3,
206 },
207 observability: ObservabilityConfig {
208 enable_tracing: true,
209 enable_metrics: true,
210 enable_logging: true,
211 log_level: "info".to_string(),
212 metrics_port: 9090,
213 traces_endpoint: Some("http://localhost:4317".to_string()),
214 },
215 plugins: PluginConfig {
216 auto_discover: true,
217 plugin_dir: "./plugins".to_string(),
218 enabled_plugins: vec![
219 "surrealdb".to_string(),
220 "postgres".to_string(),
221 "redis".to_string(),
222 ],
223 },
224 performance: PerformanceConfig {
225 container_pool_size: 5,
226 lazy_initialization: true,
227 cache_compiled_tests: true,
228 parallel_step_execution: false,
229 },
230 test_execution: TestExecutionConfig {
231 default_timeout: Duration::from_secs(300),
232 step_timeout: Duration::from_secs(60),
233 retry_on_failure: false,
234 retry_count: 3,
235 test_dir: "./tests".to_string(),
236 },
237 reporting: ReportingConfig {
238 generate_html: true,
239 generate_junit: false,
240 report_dir: "./reports".to_string(),
241 include_timestamps: true,
242 include_logs: true,
243 },
244 security: SecurityConfig {
245 hermetic_isolation: true,
246 network_isolation: true,
247 file_system_isolation: true,
248 security_level: "medium".to_string(),
249 },
250 }
251 }
252}
253
254impl CleanroomConfig {
255 pub fn validate(&self) -> Result<()> {
257 if self.project.name.trim().is_empty() {
259 return Err(CleanroomError::validation_error(
260 "Project name cannot be empty",
261 ));
262 }
263
264 if self.cli.jobs == 0 {
266 return Err(CleanroomError::validation_error(
267 "CLI jobs must be greater than 0",
268 ));
269 }
270
271 if self.containers.max_containers == 0 {
273 return Err(CleanroomError::validation_error(
274 "Max containers must be greater than 0",
275 ));
276 }
277
278 if self.containers.startup_timeout.as_secs() == 0 {
279 return Err(CleanroomError::validation_error(
280 "Container startup timeout must be greater than 0",
281 ));
282 }
283
284 if self.observability.metrics_port == 0 {
286 return Err(CleanroomError::validation_error(
287 "Metrics port must be greater than 0",
288 ));
289 }
290
291 match self.security.security_level.to_lowercase().as_str() {
293 "low" | "medium" | "high" => {}
294 _ => {
295 return Err(CleanroomError::validation_error(
296 "Security level must be 'low', 'medium', or 'high'",
297 ))
298 }
299 }
300
301 match self.observability.log_level.to_lowercase().as_str() {
303 "debug" | "info" | "warn" | "error" => {}
304 _ => {
305 return Err(CleanroomError::validation_error(
306 "Log level must be 'debug', 'info', 'warn', or 'error'",
307 ))
308 }
309 }
310
311 Ok(())
312 }
313}
314
315pub fn load_cleanroom_config_from_file<P: AsRef<Path>>(path: P) -> Result<CleanroomConfig> {
317 let path = path.as_ref();
318 let content = std::fs::read_to_string(path).map_err(|e| {
319 CleanroomError::config_error(format!("Failed to read cleanroom.toml: {}", e))
320 })?;
321
322 let config: CleanroomConfig = toml::from_str(&content).map_err(|e| {
323 CleanroomError::config_error(format!("Invalid cleanroom.toml format: {}", e))
324 })?;
325
326 config.validate()?;
327 Ok(config)
328}
329
330fn load_cleanroom_config_from_user_dir() -> Result<CleanroomConfig> {
332 let user_config_dir = std::env::var("HOME")
333 .map(|home| {
334 Path::new(&home)
335 .join(".config")
336 .join("cleanroom")
337 .join("cleanroom.toml")
338 })
339 .unwrap_or_else(|_| Path::new("~/.config/cleanroom/cleanroom.toml").to_path_buf());
340
341 if user_config_dir.exists() {
342 load_cleanroom_config_from_file(user_config_dir)
343 } else {
344 Ok(CleanroomConfig::default())
345 }
346}
347
348fn apply_env_overrides(mut config: CleanroomConfig) -> Result<CleanroomConfig> {
350 if let Ok(parallel) = std::env::var("CLEANROOM_CLI_PARALLEL") {
352 config.cli.parallel = parallel.parse::<bool>().unwrap_or(config.cli.parallel);
353 }
354 if let Ok(jobs) = std::env::var("CLEANROOM_CLI_JOBS") {
355 config.cli.jobs = jobs.parse::<usize>().unwrap_or(config.cli.jobs);
356 }
357 if let Ok(format) = std::env::var("CLEANROOM_CLI_OUTPUT_FORMAT") {
358 config.cli.output_format = format;
359 }
360 if let Ok(fail_fast) = std::env::var("CLEANROOM_CLI_FAIL_FAST") {
361 config.cli.fail_fast = fail_fast.parse::<bool>().unwrap_or(config.cli.fail_fast);
362 }
363 if let Ok(watch) = std::env::var("CLEANROOM_CLI_WATCH") {
364 config.cli.watch = watch.parse::<bool>().unwrap_or(config.cli.watch);
365 }
366 if let Ok(interactive) = std::env::var("CLEANROOM_CLI_INTERACTIVE") {
367 config.cli.interactive = interactive
368 .parse::<bool>()
369 .unwrap_or(config.cli.interactive);
370 }
371
372 if let Ok(reuse) = std::env::var("CLEANROOM_CONTAINERS_REUSE_ENABLED") {
374 config.containers.reuse_enabled = reuse
375 .parse::<bool>()
376 .unwrap_or(config.containers.reuse_enabled);
377 }
378 if let Ok(max_containers) = std::env::var("CLEANROOM_CONTAINERS_MAX_CONTAINERS") {
379 config.containers.max_containers = max_containers
380 .parse::<usize>()
381 .unwrap_or(config.containers.max_containers);
382 }
383
384 if let Ok(tracing) = std::env::var("CLEANROOM_OBSERVABILITY_ENABLE_TRACING") {
386 config.observability.enable_tracing = tracing
387 .parse::<bool>()
388 .unwrap_or(config.observability.enable_tracing);
389 }
390 if let Ok(metrics) = std::env::var("CLEANROOM_OBSERVABILITY_ENABLE_METRICS") {
391 config.observability.enable_metrics = metrics
392 .parse::<bool>()
393 .unwrap_or(config.observability.enable_metrics);
394 }
395 if let Ok(logging) = std::env::var("CLEANROOM_OBSERVABILITY_ENABLE_LOGGING") {
396 config.observability.enable_logging = logging
397 .parse::<bool>()
398 .unwrap_or(config.observability.enable_logging);
399 }
400 if let Ok(log_level) = std::env::var("CLEANROOM_OBSERVABILITY_LOG_LEVEL") {
401 config.observability.log_level = log_level;
402 }
403
404 if let Ok(hermetic) = std::env::var("CLEANROOM_SECURITY_HERMETIC_ISOLATION") {
406 config.security.hermetic_isolation = hermetic
407 .parse::<bool>()
408 .unwrap_or(config.security.hermetic_isolation);
409 }
410 if let Ok(network) = std::env::var("CLEANROOM_SECURITY_NETWORK_ISOLATION") {
411 config.security.network_isolation = network
412 .parse::<bool>()
413 .unwrap_or(config.security.network_isolation);
414 }
415 if let Ok(filesystem) = std::env::var("CLEANROOM_SECURITY_FILESYSTEM_ISOLATION") {
416 config.security.file_system_isolation = filesystem
417 .parse::<bool>()
418 .unwrap_or(config.security.file_system_isolation);
419 }
420 if let Ok(security_level) = std::env::var("CLEANROOM_SECURITY_LEVEL") {
421 config.security.security_level = security_level;
422 }
423
424 Ok(config)
425}
426
427fn merge_configs(mut base: CleanroomConfig, override_config: CleanroomConfig) -> CleanroomConfig {
429 if !override_config.project.name.trim().is_empty() {
431 base.project.name = override_config.project.name;
432 }
433 if let Some(version) = override_config.project.version {
434 base.project.version = Some(version);
435 }
436 if let Some(description) = override_config.project.description {
437 base.project.description = Some(description);
438 }
439
440 base.cli = override_config.cli;
442 base.containers = override_config.containers;
443 base.services = override_config.services;
444 base.observability = override_config.observability;
445 base.plugins = override_config.plugins;
446 base.performance = override_config.performance;
447 base.test_execution = override_config.test_execution;
448 base.reporting = override_config.reporting;
449 base.security = override_config.security;
450
451 base
452}
453
454pub fn load_cleanroom_config() -> Result<CleanroomConfig> {
456 let mut config = CleanroomConfig::default();
457
458 config = apply_env_overrides(config)?;
460
461 if let Ok(project_config) = load_cleanroom_config_from_file("cleanroom.toml") {
463 config = merge_configs(config, project_config);
464 }
465
466 if let Ok(user_config) = load_cleanroom_config_from_user_dir() {
468 config = merge_configs(config, user_config);
469 }
470
471 config.validate()?;
473
474 Ok(config)
475}