clnrm_core/config/
project.rs

1//! Cleanroom project configuration
2//!
3//! Controls framework behavior, CLI defaults, and feature toggles.
4
5use crate::error::{CleanroomError, Result};
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8use std::time::Duration;
9
10/// Cleanroom project configuration structure
11#[derive(Debug, Deserialize, Serialize, Clone)]
12pub struct CleanroomConfig {
13    /// Project metadata
14    pub project: ProjectConfig,
15    /// CLI defaults and settings
16    pub cli: CliConfig,
17    /// Container management settings
18    pub containers: ContainerConfig,
19    /// Service configuration defaults
20    pub services: ServiceDefaultsConfig,
21    /// Observability settings
22    pub observability: ObservabilityConfig,
23    /// Plugin configuration
24    pub plugins: PluginConfig,
25    /// Performance tuning options
26    pub performance: PerformanceConfig,
27    /// Test execution defaults
28    pub test_execution: TestExecutionConfig,
29    /// Reporting configuration
30    pub reporting: ReportingConfig,
31    /// Security and isolation settings
32    pub security: SecurityConfig,
33}
34
35/// Project metadata configuration
36#[derive(Debug, Deserialize, Serialize, Clone)]
37pub struct ProjectConfig {
38    /// Project name
39    pub name: String,
40    /// Project version
41    pub version: Option<String>,
42    /// Project description
43    pub description: Option<String>,
44}
45
46/// CLI configuration defaults
47#[derive(Debug, Deserialize, Serialize, Clone)]
48pub struct CliConfig {
49    /// Enable parallel test execution by default
50    pub parallel: bool,
51    /// Number of parallel workers
52    pub jobs: usize,
53    /// Default output format
54    pub output_format: String,
55    /// Stop on first failure
56    pub fail_fast: bool,
57    /// Enable watch mode for development
58    pub watch: bool,
59    /// Enable interactive debugging mode
60    pub interactive: bool,
61}
62
63/// Container management configuration
64#[derive(Debug, Deserialize, Serialize, Clone)]
65pub struct ContainerConfig {
66    /// Enable container reuse (10-50x performance improvement)
67    pub reuse_enabled: bool,
68    /// Default container image
69    pub default_image: String,
70    /// Image pull policy
71    pub pull_policy: String,
72    /// Container cleanup policy
73    pub cleanup_policy: String,
74    /// Maximum concurrent containers
75    pub max_containers: usize,
76    /// Container startup timeout
77    #[serde(deserialize_with = "super::deserializers::deserialize_duration")]
78    pub startup_timeout: Duration,
79}
80
81/// Service defaults configuration
82#[derive(Debug, Deserialize, Serialize, Clone)]
83pub struct ServiceDefaultsConfig {
84    /// Default service operation timeout
85    #[serde(deserialize_with = "super::deserializers::deserialize_duration")]
86    pub default_timeout: Duration,
87    /// Health check interval
88    #[serde(deserialize_with = "super::deserializers::deserialize_duration")]
89    pub health_check_interval: Duration,
90    /// Health check timeout
91    #[serde(deserialize_with = "super::deserializers::deserialize_duration")]
92    pub health_check_timeout: Duration,
93    /// Maximum service start retries
94    pub max_retries: u32,
95}
96
97/// Observability configuration
98#[derive(Debug, Deserialize, Serialize, Clone)]
99pub struct ObservabilityConfig {
100    /// Enable OpenTelemetry tracing
101    pub enable_tracing: bool,
102    /// Enable metrics collection
103    pub enable_metrics: bool,
104    /// Enable structured logging
105    pub enable_logging: bool,
106    /// Log level (debug, info, warn, error)
107    pub log_level: String,
108    /// Prometheus metrics port
109    pub metrics_port: u16,
110    /// OTLP traces endpoint
111    pub traces_endpoint: Option<String>,
112}
113
114/// Plugin configuration
115#[derive(Debug, Deserialize, Serialize, Clone)]
116pub struct PluginConfig {
117    /// Auto-discover plugins
118    pub auto_discover: bool,
119    /// Custom plugin directory
120    pub plugin_dir: String,
121    /// List of enabled plugins
122    pub enabled_plugins: Vec<String>,
123}
124
125/// Performance tuning configuration
126#[derive(Debug, Deserialize, Serialize, Clone)]
127pub struct PerformanceConfig {
128    /// Pre-warmed container pool size
129    pub container_pool_size: usize,
130    /// Lazy service initialization
131    pub lazy_initialization: bool,
132    /// Cache compiled test configurations
133    pub cache_compiled_tests: bool,
134    /// Execute test steps in parallel
135    pub parallel_step_execution: bool,
136}
137
138/// Test execution defaults
139#[derive(Debug, Deserialize, Serialize, Clone)]
140pub struct TestExecutionConfig {
141    /// Overall test timeout
142    #[serde(deserialize_with = "super::deserializers::deserialize_duration")]
143    pub default_timeout: Duration,
144    /// Individual step timeout
145    #[serde(deserialize_with = "super::deserializers::deserialize_duration")]
146    pub step_timeout: Duration,
147    /// Retry failed tests
148    pub retry_on_failure: bool,
149    /// Number of retries
150    pub retry_count: u32,
151    /// Default test directory
152    pub test_dir: String,
153}
154
155/// Reporting configuration
156#[derive(Debug, Deserialize, Serialize, Clone)]
157pub struct ReportingConfig {
158    /// Generate HTML reports
159    pub generate_html: bool,
160    /// Generate JUnit XML reports
161    pub generate_junit: bool,
162    /// Report output directory
163    pub report_dir: String,
164    /// Include timestamps in reports
165    pub include_timestamps: bool,
166    /// Include service logs in reports
167    pub include_logs: bool,
168}
169
170/// Security and isolation configuration
171#[derive(Debug, Deserialize, Serialize, Clone)]
172pub struct SecurityConfig {
173    /// Enforce hermetic isolation
174    pub hermetic_isolation: bool,
175    /// Isolate container networks
176    pub network_isolation: bool,
177    /// Isolate file systems
178    pub file_system_isolation: bool,
179    /// Security level (low, medium, high)
180    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    /// Validate the configuration
262    pub fn validate(&self) -> Result<()> {
263        // Validate project name
264        if self.project.name.trim().is_empty() {
265            return Err(CleanroomError::validation_error(
266                "Project name cannot be empty",
267            ));
268        }
269
270        // Validate CLI settings
271        if self.cli.jobs == 0 {
272            return Err(CleanroomError::validation_error(
273                "CLI jobs must be greater than 0",
274            ));
275        }
276
277        // Validate container settings
278        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        // Validate observability settings
291        if self.observability.metrics_port == 0 {
292            return Err(CleanroomError::validation_error(
293                "Metrics port must be greater than 0",
294            ));
295        }
296
297        // Validate security level
298        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        // Validate log level
308        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
321/// Load CleanroomConfig from a specific file
322pub 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
336/// Load CleanroomConfig from user directory
337fn 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
354/// Apply environment variable overrides to configuration
355fn apply_env_overrides(mut config: CleanroomConfig) -> Result<CleanroomConfig> {
356    // CLI settings
357    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    // Container settings
379    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    // Observability settings
394    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    // Security settings
414    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
436/// Merge two configurations, with the second taking priority
437fn merge_configs(mut base: CleanroomConfig, override_config: CleanroomConfig) -> CleanroomConfig {
438    // Project metadata (override takes priority)
439    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    // CLI settings (override takes priority)
450    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
463/// Load CleanroomConfig from file with priority system
464pub fn load_cleanroom_config() -> Result<CleanroomConfig> {
465    let mut config = CleanroomConfig::default();
466
467    // Priority 1 (lowest): User cleanroom.toml (~/.config/cleanroom/cleanroom.toml)
468    if let Ok(user_config) = load_cleanroom_config_from_user_dir() {
469        config = merge_configs(config, user_config);
470    }
471
472    // Priority 2: Project cleanroom.toml (./cleanroom.toml)
473    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    // Priority 3 (highest): Environment variables (CLEANROOM_*)
484    config = apply_env_overrides(config)?;
485
486    // Validate final configuration
487    config.validate()?;
488
489    Ok(config)
490}