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    pub startup_timeout: Duration,
78}
79
80/// Service defaults configuration
81#[derive(Debug, Deserialize, Serialize, Clone)]
82pub struct ServiceDefaultsConfig {
83    /// Default service operation timeout
84    pub default_timeout: Duration,
85    /// Health check interval
86    pub health_check_interval: Duration,
87    /// Health check timeout
88    pub health_check_timeout: Duration,
89    /// Maximum service start retries
90    pub max_retries: u32,
91}
92
93/// Observability configuration
94#[derive(Debug, Deserialize, Serialize, Clone)]
95pub struct ObservabilityConfig {
96    /// Enable OpenTelemetry tracing
97    pub enable_tracing: bool,
98    /// Enable metrics collection
99    pub enable_metrics: bool,
100    /// Enable structured logging
101    pub enable_logging: bool,
102    /// Log level (debug, info, warn, error)
103    pub log_level: String,
104    /// Prometheus metrics port
105    pub metrics_port: u16,
106    /// OTLP traces endpoint
107    pub traces_endpoint: Option<String>,
108}
109
110/// Plugin configuration
111#[derive(Debug, Deserialize, Serialize, Clone)]
112pub struct PluginConfig {
113    /// Auto-discover plugins
114    pub auto_discover: bool,
115    /// Custom plugin directory
116    pub plugin_dir: String,
117    /// List of enabled plugins
118    pub enabled_plugins: Vec<String>,
119}
120
121/// Performance tuning configuration
122#[derive(Debug, Deserialize, Serialize, Clone)]
123pub struct PerformanceConfig {
124    /// Pre-warmed container pool size
125    pub container_pool_size: usize,
126    /// Lazy service initialization
127    pub lazy_initialization: bool,
128    /// Cache compiled test configurations
129    pub cache_compiled_tests: bool,
130    /// Execute test steps in parallel
131    pub parallel_step_execution: bool,
132}
133
134/// Test execution defaults
135#[derive(Debug, Deserialize, Serialize, Clone)]
136pub struct TestExecutionConfig {
137    /// Overall test timeout
138    pub default_timeout: Duration,
139    /// Individual step timeout
140    pub step_timeout: Duration,
141    /// Retry failed tests
142    pub retry_on_failure: bool,
143    /// Number of retries
144    pub retry_count: u32,
145    /// Default test directory
146    pub test_dir: String,
147}
148
149/// Reporting configuration
150#[derive(Debug, Deserialize, Serialize, Clone)]
151pub struct ReportingConfig {
152    /// Generate HTML reports
153    pub generate_html: bool,
154    /// Generate JUnit XML reports
155    pub generate_junit: bool,
156    /// Report output directory
157    pub report_dir: String,
158    /// Include timestamps in reports
159    pub include_timestamps: bool,
160    /// Include service logs in reports
161    pub include_logs: bool,
162}
163
164/// Security and isolation configuration
165#[derive(Debug, Deserialize, Serialize, Clone)]
166pub struct SecurityConfig {
167    /// Enforce hermetic isolation
168    pub hermetic_isolation: bool,
169    /// Isolate container networks
170    pub network_isolation: bool,
171    /// Isolate file systems
172    pub file_system_isolation: bool,
173    /// Security level (low, medium, high)
174    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    /// Validate the configuration
256    pub fn validate(&self) -> Result<()> {
257        // Validate project name
258        if self.project.name.trim().is_empty() {
259            return Err(CleanroomError::validation_error(
260                "Project name cannot be empty",
261            ));
262        }
263
264        // Validate CLI settings
265        if self.cli.jobs == 0 {
266            return Err(CleanroomError::validation_error(
267                "CLI jobs must be greater than 0",
268            ));
269        }
270
271        // Validate container settings
272        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        // Validate observability settings
285        if self.observability.metrics_port == 0 {
286            return Err(CleanroomError::validation_error(
287                "Metrics port must be greater than 0",
288            ));
289        }
290
291        // Validate security level
292        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        // Validate log level
302        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
315/// Load CleanroomConfig from a specific file
316pub 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
330/// Load CleanroomConfig from user directory
331fn 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
348/// Apply environment variable overrides to configuration
349fn apply_env_overrides(mut config: CleanroomConfig) -> Result<CleanroomConfig> {
350    // CLI settings
351    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    // Container settings
373    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    // Observability settings
385    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    // Security settings
405    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
427/// Merge two configurations, with the second taking priority
428fn merge_configs(mut base: CleanroomConfig, override_config: CleanroomConfig) -> CleanroomConfig {
429    // Project metadata (override takes priority)
430    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    // CLI settings (override takes priority)
441    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
454/// Load CleanroomConfig from file with priority system
455pub fn load_cleanroom_config() -> Result<CleanroomConfig> {
456    let mut config = CleanroomConfig::default();
457
458    // Priority 1: Environment variables (CLEANROOM_*)
459    config = apply_env_overrides(config)?;
460
461    // Priority 2: Project cleanroom.toml (./cleanroom.toml)
462    if let Ok(project_config) = load_cleanroom_config_from_file("cleanroom.toml") {
463        config = merge_configs(config, project_config);
464    }
465
466    // Priority 3: User cleanroom.toml (~/.config/cleanroom/cleanroom.toml)
467    if let Ok(user_config) = load_cleanroom_config_from_user_dir() {
468        config = merge_configs(config, user_config);
469    }
470
471    // Validate final configuration
472    config.validate()?;
473
474    Ok(config)
475}