clnrm_core/
config.rs

1//! Configuration system for cleanroom testing
2//!
3//! Provides TOML-based configuration parsing and validation for test files
4//! and cleanroom environment settings.
5
6use crate::error::{CleanroomError, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::Path;
10use std::time::Duration;
11
12/// Main test configuration structure
13#[derive(Debug, Deserialize, Serialize, Clone)]
14pub struct TestConfig {
15    /// Test metadata section
16    pub test: TestMetadataSection,
17    /// Service configurations (as a table)
18    pub services: Option<HashMap<String, ServiceConfig>>,
19    /// Test steps to execute
20    pub steps: Vec<StepConfig>,
21    /// Assertions
22    pub assertions: Option<HashMap<String, serde_json::Value>>,
23    /// OpenTelemetry validation configuration
24    pub otel_validation: Option<OtelValidationSection>,
25}
26
27/// Test metadata section
28#[derive(Debug, Deserialize, Serialize, Clone)]
29pub struct TestMetadataSection {
30    /// Test metadata
31    pub metadata: TestMetadata,
32}
33
34/// Test metadata configuration
35#[derive(Debug, Deserialize, Serialize, Clone)]
36pub struct TestMetadata {
37    /// Test name
38    pub name: String,
39    /// Test description
40    pub description: Option<String>,
41    /// Test timeout
42    pub timeout: Option<String>,
43}
44
45/// Individual test scenario configuration
46#[derive(Debug, Deserialize, Serialize, Clone)]
47pub struct ScenarioConfig {
48    /// Scenario name
49    pub name: String,
50    /// Test steps to execute
51    pub steps: Vec<StepConfig>,
52    /// Whether to run steps concurrently
53    pub concurrent: Option<bool>,
54    /// Scenario-specific timeout
55    pub timeout_ms: Option<u64>,
56    /// Scenario-specific policy
57    pub policy: Option<PolicyConfig>,
58}
59
60/// Individual test step configuration
61#[derive(Debug, Deserialize, Serialize, Clone)]
62pub struct StepConfig {
63    /// Step name
64    pub name: String,
65    /// Command to execute
66    pub command: Vec<String>,
67    /// Expected output regex pattern
68    pub expected_output_regex: Option<String>,
69    /// Working directory
70    pub workdir: Option<String>,
71    /// Step-specific environment variables
72    pub env: Option<HashMap<String, String>>,
73    /// Expected exit code (default: 0)
74    pub expected_exit_code: Option<i32>,
75    /// Whether to continue on failure
76    pub continue_on_failure: Option<bool>,
77    /// Service to execute command on (optional)
78    pub service: Option<String>,
79}
80
81/// Security policy configuration
82#[derive(Debug, Deserialize, Serialize, Clone)]
83pub struct PolicyConfig {
84    /// Security level
85    pub security_level: Option<String>,
86    /// Maximum execution time in seconds
87    pub max_execution_time: Option<u64>,
88    /// Maximum memory usage in MB
89    pub max_memory_mb: Option<u64>,
90    /// Maximum CPU usage (0.0-1.0)
91    pub max_cpu_usage: Option<f64>,
92    /// Allowed network hosts
93    pub allowed_network_hosts: Option<Vec<String>>,
94    /// Disallowed commands
95    pub disallowed_commands: Option<Vec<String>>,
96}
97
98/// Service configuration
99#[derive(Debug, Deserialize, Serialize, Clone)]
100pub struct ServiceConfig {
101    /// Service type (generic_container, database, ollama, etc.)
102    pub r#type: String,
103    /// Service plugin
104    pub plugin: String,
105    /// Service image (optional for network services)
106    pub image: Option<String>,
107    /// Service environment variables
108    pub env: Option<HashMap<String, String>>,
109    /// Service ports
110    pub ports: Option<Vec<u16>>,
111    /// Service volumes
112    pub volumes: Option<Vec<VolumeConfig>>,
113    /// Service health check
114    pub health_check: Option<HealthCheckConfig>,
115    /// SurrealDB username (optional, defaults to root)
116    pub username: Option<String>,
117    /// SurrealDB password (optional, defaults to root)
118    pub password: Option<String>,
119    /// SurrealDB strict mode (optional, defaults to false)
120    pub strict: Option<bool>,
121}
122
123/// Volume configuration
124#[derive(Debug, Deserialize, Serialize, Clone)]
125pub struct VolumeConfig {
126    /// Host path
127    pub host_path: String,
128    /// Container path
129    pub container_path: String,
130    /// Whether volume is read-only
131    pub read_only: Option<bool>,
132}
133
134impl VolumeConfig {
135    /// Validate the volume configuration
136    ///
137    /// # Errors
138    ///
139    /// Returns error if:
140    /// - Host path is empty
141    /// - Container path is empty
142    /// - Paths contain invalid characters
143    pub fn validate(&self) -> Result<()> {
144        use std::path::Path;
145
146        // Validate host path is not empty
147        if self.host_path.trim().is_empty() {
148            return Err(CleanroomError::validation_error(
149                "Volume host path cannot be empty",
150            ));
151        }
152
153        // Validate container path is not empty
154        if self.container_path.trim().is_empty() {
155            return Err(CleanroomError::validation_error(
156                "Volume container path cannot be empty",
157            ));
158        }
159
160        // Validate host path is absolute
161        let host_path = Path::new(&self.host_path);
162        if !host_path.is_absolute() {
163            return Err(CleanroomError::validation_error(format!(
164                "Volume host path must be absolute: {}",
165                self.host_path
166            )));
167        }
168
169        // Validate container path is absolute
170        let container_path = Path::new(&self.container_path);
171        if !container_path.is_absolute() {
172            return Err(CleanroomError::validation_error(format!(
173                "Volume container path must be absolute: {}",
174                self.container_path
175            )));
176        }
177
178        Ok(())
179    }
180
181    /// Convert to VolumeMount with validation
182    ///
183    /// This helper method creates a VolumeMount from the configuration
184    /// with full validation including path existence checks.
185    pub fn to_volume_mount(&self) -> Result<crate::backend::volume::VolumeMount> {
186        use crate::backend::volume::VolumeMount;
187        VolumeMount::from_config(self)
188    }
189}
190
191/// Health check configuration
192#[derive(Debug, Deserialize, Serialize, Clone)]
193pub struct HealthCheckConfig {
194    /// Health check command
195    pub cmd: Vec<String>,
196    /// Health check interval in seconds
197    pub interval: Option<u64>,
198    /// Health check timeout in seconds
199    pub timeout: Option<u64>,
200    /// Number of retries
201    pub retries: Option<u32>,
202}
203
204/// Timeout configuration
205#[derive(Debug, Deserialize, Serialize, Clone)]
206pub struct TimeoutConfig {
207    /// Default step timeout in milliseconds
208    pub step_timeout_ms: Option<u64>,
209    /// Default scenario timeout in milliseconds
210    pub scenario_timeout_ms: Option<u64>,
211    /// Global test timeout in milliseconds
212    pub test_timeout_ms: Option<u64>,
213}
214
215/// Cleanroom project configuration structure
216/// Controls framework behavior, CLI defaults, and feature toggles
217#[derive(Debug, Deserialize, Serialize, Clone)]
218pub struct CleanroomConfig {
219    /// Project metadata
220    pub project: ProjectConfig,
221    /// CLI defaults and settings
222    pub cli: CliConfig,
223    /// Container management settings
224    pub containers: ContainerConfig,
225    /// Service configuration defaults
226    pub services: ServiceDefaultsConfig,
227    /// Observability settings
228    pub observability: ObservabilityConfig,
229    /// Plugin configuration
230    pub plugins: PluginConfig,
231    /// Performance tuning options
232    pub performance: PerformanceConfig,
233    /// Test execution defaults
234    pub test_execution: TestExecutionConfig,
235    /// Reporting configuration
236    pub reporting: ReportingConfig,
237    /// Security and isolation settings
238    pub security: SecurityConfig,
239}
240
241/// Project metadata configuration
242#[derive(Debug, Deserialize, Serialize, Clone)]
243pub struct ProjectConfig {
244    /// Project name
245    pub name: String,
246    /// Project version
247    pub version: Option<String>,
248    /// Project description
249    pub description: Option<String>,
250}
251
252/// CLI configuration defaults
253#[derive(Debug, Deserialize, Serialize, Clone)]
254pub struct CliConfig {
255    /// Enable parallel test execution by default
256    pub parallel: bool,
257    /// Number of parallel workers
258    pub jobs: usize,
259    /// Default output format
260    pub output_format: String,
261    /// Stop on first failure
262    pub fail_fast: bool,
263    /// Enable watch mode for development
264    pub watch: bool,
265    /// Enable interactive debugging mode
266    pub interactive: bool,
267}
268
269/// Container management configuration
270#[derive(Debug, Deserialize, Serialize, Clone)]
271pub struct ContainerConfig {
272    /// Enable container reuse (10-50x performance improvement)
273    pub reuse_enabled: bool,
274    /// Default container image
275    pub default_image: String,
276    /// Image pull policy
277    pub pull_policy: String,
278    /// Container cleanup policy
279    pub cleanup_policy: String,
280    /// Maximum concurrent containers
281    pub max_containers: usize,
282    /// Container startup timeout
283    pub startup_timeout: Duration,
284}
285
286/// Service defaults configuration
287#[derive(Debug, Deserialize, Serialize, Clone)]
288pub struct ServiceDefaultsConfig {
289    /// Default service operation timeout
290    pub default_timeout: Duration,
291    /// Health check interval
292    pub health_check_interval: Duration,
293    /// Health check timeout
294    pub health_check_timeout: Duration,
295    /// Maximum service start retries
296    pub max_retries: u32,
297}
298
299/// Observability configuration
300#[derive(Debug, Deserialize, Serialize, Clone)]
301pub struct ObservabilityConfig {
302    /// Enable OpenTelemetry tracing
303    pub enable_tracing: bool,
304    /// Enable metrics collection
305    pub enable_metrics: bool,
306    /// Enable structured logging
307    pub enable_logging: bool,
308    /// Log level (debug, info, warn, error)
309    pub log_level: String,
310    /// Prometheus metrics port
311    pub metrics_port: u16,
312    /// OTLP traces endpoint
313    pub traces_endpoint: Option<String>,
314}
315
316/// Plugin configuration
317#[derive(Debug, Deserialize, Serialize, Clone)]
318pub struct PluginConfig {
319    /// Auto-discover plugins
320    pub auto_discover: bool,
321    /// Custom plugin directory
322    pub plugin_dir: String,
323    /// List of enabled plugins
324    pub enabled_plugins: Vec<String>,
325}
326
327/// Performance tuning configuration
328#[derive(Debug, Deserialize, Serialize, Clone)]
329pub struct PerformanceConfig {
330    /// Pre-warmed container pool size
331    pub container_pool_size: usize,
332    /// Lazy service initialization
333    pub lazy_initialization: bool,
334    /// Cache compiled test configurations
335    pub cache_compiled_tests: bool,
336    /// Execute test steps in parallel
337    pub parallel_step_execution: bool,
338}
339
340/// Test execution defaults
341#[derive(Debug, Deserialize, Serialize, Clone)]
342pub struct TestExecutionConfig {
343    /// Overall test timeout
344    pub default_timeout: Duration,
345    /// Individual step timeout
346    pub step_timeout: Duration,
347    /// Retry failed tests
348    pub retry_on_failure: bool,
349    /// Number of retries
350    pub retry_count: u32,
351    /// Default test directory
352    pub test_dir: String,
353}
354
355/// Reporting configuration
356#[derive(Debug, Deserialize, Serialize, Clone)]
357pub struct ReportingConfig {
358    /// Generate HTML reports
359    pub generate_html: bool,
360    /// Generate JUnit XML reports
361    pub generate_junit: bool,
362    /// Report output directory
363    pub report_dir: String,
364    /// Include timestamps in reports
365    pub include_timestamps: bool,
366    /// Include service logs in reports
367    pub include_logs: bool,
368}
369
370/// Security and isolation configuration
371#[derive(Debug, Deserialize, Serialize, Clone)]
372pub struct SecurityConfig {
373    /// Enforce hermetic isolation
374    pub hermetic_isolation: bool,
375    /// Isolate container networks
376    pub network_isolation: bool,
377    /// Isolate file systems
378    pub file_system_isolation: bool,
379    /// Security level (low, medium, high)
380    pub security_level: String,
381}
382
383/// OpenTelemetry validation section in TOML
384#[derive(Debug, Deserialize, Serialize, Clone)]
385pub struct OtelValidationSection {
386    /// Enable OTEL validation
387    pub enabled: bool,
388    /// Validate spans
389    pub validate_spans: Option<bool>,
390    /// Validate traces
391    pub validate_traces: Option<bool>,
392    /// Validate exports
393    pub validate_exports: Option<bool>,
394    /// Validate performance overhead
395    pub validate_performance: Option<bool>,
396    /// Maximum allowed performance overhead in milliseconds
397    pub max_overhead_ms: Option<f64>,
398    /// Expected spans configuration
399    pub expected_spans: Option<Vec<ExpectedSpanConfig>>,
400    /// Expected traces configuration
401    pub expected_traces: Option<Vec<ExpectedTraceConfig>>,
402}
403
404/// Expected span configuration from TOML
405#[derive(Debug, Deserialize, Serialize, Clone)]
406pub struct ExpectedSpanConfig {
407    /// Span name (operation name)
408    pub name: String,
409    /// Expected attributes
410    pub attributes: Option<HashMap<String, String>>,
411    /// Whether span is required
412    pub required: Option<bool>,
413    /// Minimum duration in milliseconds
414    pub min_duration_ms: Option<f64>,
415    /// Maximum duration in milliseconds
416    pub max_duration_ms: Option<f64>,
417}
418
419/// Expected trace configuration from TOML
420#[derive(Debug, Deserialize, Serialize, Clone)]
421pub struct ExpectedTraceConfig {
422    /// Trace ID (optional, for specific trace validation)
423    pub trace_id: Option<String>,
424    /// Expected span names in the trace
425    pub span_names: Vec<String>,
426    /// Whether all spans must be present
427    pub complete: Option<bool>,
428    /// Parent-child relationships (parent_name -> child_name)
429    pub parent_child: Option<Vec<(String, String)>>,
430}
431
432impl TestConfig {
433    /// Validate the configuration
434    pub fn validate(&self) -> Result<()> {
435        // Validate name is not empty
436        if self.test.metadata.name.trim().is_empty() {
437            return Err(CleanroomError::validation_error(
438                "Test name cannot be empty",
439            ));
440        }
441
442        // Validate steps exist
443        if self.steps.is_empty() {
444            return Err(CleanroomError::validation_error(
445                "At least one step is required",
446            ));
447        }
448
449        // Validate each step
450        for (i, step) in self.steps.iter().enumerate() {
451            step.validate()
452                .map_err(|e| CleanroomError::validation_error(format!("Step {}: {}", i, e)))?;
453        }
454
455        // Validate services if present
456        if let Some(services) = &self.services {
457            for (service_name, service) in services.iter() {
458                service.validate().map_err(|e| {
459                    CleanroomError::validation_error(format!("Service {}: {}", service_name, e))
460                })?;
461            }
462        }
463
464        // Validate assertions if present (basic validation)
465        if let Some(assertions) = &self.assertions {
466            // Basic validation - assertions should not be empty
467            if assertions.is_empty() {
468                return Err(CleanroomError::validation_error(
469                    "Assertions cannot be empty if provided",
470                ));
471            }
472        }
473
474        Ok(())
475    }
476}
477
478impl ScenarioConfig {
479    /// Validate the scenario configuration
480    pub fn validate(&self) -> Result<()> {
481        if self.name.trim().is_empty() {
482            return Err(CleanroomError::validation_error(
483                "Scenario name cannot be empty",
484            ));
485        }
486
487        if self.steps.is_empty() {
488            return Err(CleanroomError::validation_error(
489                "At least one step is required",
490            ));
491        }
492
493        for (i, step) in self.steps.iter().enumerate() {
494            step.validate()
495                .map_err(|e| CleanroomError::validation_error(format!("Step {}: {}", i, e)))?;
496        }
497
498        Ok(())
499    }
500}
501
502impl StepConfig {
503    /// Validate the step configuration
504    pub fn validate(&self) -> Result<()> {
505        if self.name.trim().is_empty() {
506            return Err(CleanroomError::validation_error(
507                "Step name cannot be empty",
508            ));
509        }
510
511        if self.command.is_empty() {
512            return Err(CleanroomError::validation_error(
513                "Step command cannot be empty",
514            ));
515        }
516
517        Ok(())
518    }
519}
520
521impl ServiceConfig {
522    /// Validate the service configuration
523    pub fn validate(&self) -> Result<()> {
524        if self.r#type.trim().is_empty() {
525            return Err(CleanroomError::validation_error(
526                "Service type cannot be empty",
527            ));
528        }
529
530        if self.plugin.trim().is_empty() {
531            return Err(CleanroomError::validation_error(
532                "Service plugin cannot be empty",
533            ));
534        }
535
536        if let Some(ref image) = self.image {
537            if image.trim().is_empty() {
538                return Err(CleanroomError::validation_error(
539                    "Service image cannot be empty",
540                ));
541            }
542        } else if self.r#type != "network_service" && self.r#type != "ollama" {
543            // For container-based services, image is required
544            return Err(CleanroomError::validation_error(
545                "Service image is required for container-based services",
546            ));
547        }
548
549        // Validate volumes if present
550        if let Some(ref volumes) = self.volumes {
551            for (i, volume) in volumes.iter().enumerate() {
552                volume.validate().map_err(|e| {
553                    CleanroomError::validation_error(format!("Volume {}: {}", i, e))
554                })?;
555            }
556        }
557
558        Ok(())
559    }
560}
561
562impl PolicyConfig {
563    /// Validate the policy configuration
564    pub fn validate(&self) -> Result<()> {
565        if let Some(security_level) = &self.security_level {
566            match security_level.to_lowercase().as_str() {
567                "low" | "medium" | "high" => {}
568                _ => {
569                    return Err(CleanroomError::validation_error(
570                        "Security level must be 'low', 'medium', or 'high'",
571                    ))
572                }
573            }
574        }
575
576        if let Some(max_cpu) = self.max_cpu_usage {
577            if !(0.0..=1.0).contains(&max_cpu) {
578                return Err(CleanroomError::validation_error(
579                    "Max CPU usage must be between 0.0 and 1.0",
580                ));
581            }
582        }
583
584        Ok(())
585    }
586}
587
588/// Parse TOML configuration from string
589pub fn parse_toml_config(content: &str) -> Result<TestConfig> {
590    toml::from_str::<TestConfig>(content)
591        .map_err(|e| CleanroomError::config_error(format!("TOML parse error: {}", e)))
592}
593
594/// Load configuration from file
595pub fn load_config_from_file(path: &std::path::Path) -> Result<TestConfig> {
596    let content = std::fs::read_to_string(path)
597        .map_err(|e| CleanroomError::config_error(format!("Failed to read config file: {}", e)))?;
598
599    let config = parse_toml_config(&content)?;
600    config.validate()?;
601
602    Ok(config)
603}
604
605/// Default implementation for CleanroomConfig
606impl Default for CleanroomConfig {
607    fn default() -> Self {
608        Self {
609            project: ProjectConfig {
610                name: "cleanroom-project".to_string(),
611                version: Some("0.1.0".to_string()),
612                description: Some("Cleanroom integration tests".to_string()),
613            },
614            cli: CliConfig {
615                parallel: false,
616                jobs: 4,
617                output_format: "human".to_string(),
618                fail_fast: false,
619                watch: false,
620                interactive: false,
621            },
622            containers: ContainerConfig {
623                reuse_enabled: true,
624                default_image: "alpine:latest".to_string(),
625                pull_policy: "if-not-present".to_string(),
626                cleanup_policy: "on-success".to_string(),
627                max_containers: 10,
628                startup_timeout: Duration::from_secs(60),
629            },
630            services: ServiceDefaultsConfig {
631                default_timeout: Duration::from_secs(30),
632                health_check_interval: Duration::from_secs(5),
633                health_check_timeout: Duration::from_secs(10),
634                max_retries: 3,
635            },
636            observability: ObservabilityConfig {
637                enable_tracing: true,
638                enable_metrics: true,
639                enable_logging: true,
640                log_level: "info".to_string(),
641                metrics_port: 9090,
642                traces_endpoint: Some("http://localhost:4317".to_string()),
643            },
644            plugins: PluginConfig {
645                auto_discover: true,
646                plugin_dir: "./plugins".to_string(),
647                enabled_plugins: vec![
648                    "surrealdb".to_string(),
649                    "postgres".to_string(),
650                    "redis".to_string(),
651                ],
652            },
653            performance: PerformanceConfig {
654                container_pool_size: 5,
655                lazy_initialization: true,
656                cache_compiled_tests: true,
657                parallel_step_execution: false,
658            },
659            test_execution: TestExecutionConfig {
660                default_timeout: Duration::from_secs(300),
661                step_timeout: Duration::from_secs(60),
662                retry_on_failure: false,
663                retry_count: 3,
664                test_dir: "./tests".to_string(),
665            },
666            reporting: ReportingConfig {
667                generate_html: true,
668                generate_junit: false,
669                report_dir: "./reports".to_string(),
670                include_timestamps: true,
671                include_logs: true,
672            },
673            security: SecurityConfig {
674                hermetic_isolation: true,
675                network_isolation: true,
676                file_system_isolation: true,
677                security_level: "medium".to_string(),
678            },
679        }
680    }
681}
682
683/// Validate CleanroomConfig
684impl CleanroomConfig {
685    /// Validate the configuration
686    pub fn validate(&self) -> Result<()> {
687        // Validate project name
688        if self.project.name.trim().is_empty() {
689            return Err(CleanroomError::validation_error(
690                "Project name cannot be empty",
691            ));
692        }
693
694        // Validate CLI settings
695        if self.cli.jobs == 0 {
696            return Err(CleanroomError::validation_error(
697                "CLI jobs must be greater than 0",
698            ));
699        }
700
701        // Validate container settings
702        if self.containers.max_containers == 0 {
703            return Err(CleanroomError::validation_error(
704                "Max containers must be greater than 0",
705            ));
706        }
707
708        if self.containers.startup_timeout.as_secs() == 0 {
709            return Err(CleanroomError::validation_error(
710                "Container startup timeout must be greater than 0",
711            ));
712        }
713
714        // Validate observability settings
715        if self.observability.metrics_port == 0 {
716            return Err(CleanroomError::validation_error(
717                "Metrics port must be greater than 0",
718            ));
719        }
720
721        // Validate security level
722        match self.security.security_level.to_lowercase().as_str() {
723            "low" | "medium" | "high" => {}
724            _ => {
725                return Err(CleanroomError::validation_error(
726                    "Security level must be 'low', 'medium', or 'high'",
727                ))
728            }
729        }
730
731        // Validate log level
732        match self.observability.log_level.to_lowercase().as_str() {
733            "debug" | "info" | "warn" | "error" => {}
734            _ => {
735                return Err(CleanroomError::validation_error(
736                    "Log level must be 'debug', 'info', 'warn', or 'error'",
737                ))
738            }
739        }
740
741        Ok(())
742    }
743}
744
745/// Load CleanroomConfig from file with priority system
746pub fn load_cleanroom_config() -> Result<CleanroomConfig> {
747    let mut config = CleanroomConfig::default();
748
749    // Priority 1: Environment variables (CLEANROOM_*)
750    config = apply_env_overrides(config)?;
751
752    // Priority 2: Project cleanroom.toml (./cleanroom.toml)
753    if let Ok(project_config) = load_cleanroom_config_from_file("cleanroom.toml") {
754        config = merge_configs(config, project_config);
755    }
756
757    // Priority 3: User cleanroom.toml (~/.config/cleanroom/cleanroom.toml)
758    if let Ok(user_config) = load_cleanroom_config_from_user_dir() {
759        config = merge_configs(config, user_config);
760    }
761
762    // Validate final configuration
763    config.validate()?;
764
765    Ok(config)
766}
767
768/// Load CleanroomConfig from a specific file
769pub fn load_cleanroom_config_from_file<P: AsRef<Path>>(path: P) -> Result<CleanroomConfig> {
770    let path = path.as_ref();
771    let content = std::fs::read_to_string(path).map_err(|e| {
772        CleanroomError::config_error(format!("Failed to read cleanroom.toml: {}", e))
773    })?;
774
775    let config: CleanroomConfig = toml::from_str(&content).map_err(|e| {
776        CleanroomError::config_error(format!("Invalid cleanroom.toml format: {}", e))
777    })?;
778
779    config.validate()?;
780    Ok(config)
781}
782
783/// Load CleanroomConfig from user directory
784fn load_cleanroom_config_from_user_dir() -> Result<CleanroomConfig> {
785    let user_config_dir = std::env::var("HOME")
786        .map(|home| {
787            Path::new(&home)
788                .join(".config")
789                .join("cleanroom")
790                .join("cleanroom.toml")
791        })
792        .unwrap_or_else(|_| Path::new("~/.config/cleanroom/cleanroom.toml").to_path_buf());
793
794    if user_config_dir.exists() {
795        load_cleanroom_config_from_file(user_config_dir)
796    } else {
797        Ok(CleanroomConfig::default())
798    }
799}
800
801/// Apply environment variable overrides to configuration
802fn apply_env_overrides(mut config: CleanroomConfig) -> Result<CleanroomConfig> {
803    // CLI settings
804    if let Ok(parallel) = std::env::var("CLEANROOM_CLI_PARALLEL") {
805        config.cli.parallel = parallel.parse::<bool>().unwrap_or(config.cli.parallel);
806    }
807    if let Ok(jobs) = std::env::var("CLEANROOM_CLI_JOBS") {
808        config.cli.jobs = jobs.parse::<usize>().unwrap_or(config.cli.jobs);
809    }
810    if let Ok(format) = std::env::var("CLEANROOM_CLI_OUTPUT_FORMAT") {
811        config.cli.output_format = format;
812    }
813    if let Ok(fail_fast) = std::env::var("CLEANROOM_CLI_FAIL_FAST") {
814        config.cli.fail_fast = fail_fast.parse::<bool>().unwrap_or(config.cli.fail_fast);
815    }
816    if let Ok(watch) = std::env::var("CLEANROOM_CLI_WATCH") {
817        config.cli.watch = watch.parse::<bool>().unwrap_or(config.cli.watch);
818    }
819    if let Ok(interactive) = std::env::var("CLEANROOM_CLI_INTERACTIVE") {
820        config.cli.interactive = interactive
821            .parse::<bool>()
822            .unwrap_or(config.cli.interactive);
823    }
824
825    // Container settings
826    if let Ok(reuse) = std::env::var("CLEANROOM_CONTAINERS_REUSE_ENABLED") {
827        config.containers.reuse_enabled = reuse
828            .parse::<bool>()
829            .unwrap_or(config.containers.reuse_enabled);
830    }
831    if let Ok(max_containers) = std::env::var("CLEANROOM_CONTAINERS_MAX_CONTAINERS") {
832        config.containers.max_containers = max_containers
833            .parse::<usize>()
834            .unwrap_or(config.containers.max_containers);
835    }
836
837    // Observability settings
838    if let Ok(tracing) = std::env::var("CLEANROOM_OBSERVABILITY_ENABLE_TRACING") {
839        config.observability.enable_tracing = tracing
840            .parse::<bool>()
841            .unwrap_or(config.observability.enable_tracing);
842    }
843    if let Ok(metrics) = std::env::var("CLEANROOM_OBSERVABILITY_ENABLE_METRICS") {
844        config.observability.enable_metrics = metrics
845            .parse::<bool>()
846            .unwrap_or(config.observability.enable_metrics);
847    }
848    if let Ok(logging) = std::env::var("CLEANROOM_OBSERVABILITY_ENABLE_LOGGING") {
849        config.observability.enable_logging = logging
850            .parse::<bool>()
851            .unwrap_or(config.observability.enable_logging);
852    }
853    if let Ok(log_level) = std::env::var("CLEANROOM_OBSERVABILITY_LOG_LEVEL") {
854        config.observability.log_level = log_level;
855    }
856
857    // Security settings
858    if let Ok(hermetic) = std::env::var("CLEANROOM_SECURITY_HERMETIC_ISOLATION") {
859        config.security.hermetic_isolation = hermetic
860            .parse::<bool>()
861            .unwrap_or(config.security.hermetic_isolation);
862    }
863    if let Ok(network) = std::env::var("CLEANROOM_SECURITY_NETWORK_ISOLATION") {
864        config.security.network_isolation = network
865            .parse::<bool>()
866            .unwrap_or(config.security.network_isolation);
867    }
868    if let Ok(filesystem) = std::env::var("CLEANROOM_SECURITY_FILESYSTEM_ISOLATION") {
869        config.security.file_system_isolation = filesystem
870            .parse::<bool>()
871            .unwrap_or(config.security.file_system_isolation);
872    }
873    if let Ok(security_level) = std::env::var("CLEANROOM_SECURITY_LEVEL") {
874        config.security.security_level = security_level;
875    }
876
877    Ok(config)
878}
879
880/// Merge two configurations, with the second taking priority
881fn merge_configs(mut base: CleanroomConfig, override_config: CleanroomConfig) -> CleanroomConfig {
882    // Project metadata (override takes priority)
883    if !override_config.project.name.trim().is_empty() {
884        base.project.name = override_config.project.name;
885    }
886    if let Some(version) = override_config.project.version {
887        base.project.version = Some(version);
888    }
889    if let Some(description) = override_config.project.description {
890        base.project.description = Some(description);
891    }
892
893    // CLI settings (override takes priority)
894    base.cli = override_config.cli;
895
896    // Container settings (override takes priority)
897    base.containers = override_config.containers;
898
899    // Service settings (override takes priority)
900    base.services = override_config.services;
901
902    // Observability settings (override takes priority)
903    base.observability = override_config.observability;
904
905    // Plugin settings (override takes priority)
906    base.plugins = override_config.plugins;
907
908    // Performance settings (override takes priority)
909    base.performance = override_config.performance;
910
911    // Test execution settings (override takes priority)
912    base.test_execution = override_config.test_execution;
913
914    // Reporting settings (override takes priority)
915    base.reporting = override_config.reporting;
916
917    // Security settings (override takes priority)
918    base.security = override_config.security;
919
920    base
921}
922
923#[cfg(test)]
924mod tests {
925    use super::*;
926
927    #[test]
928    #[ignore = "Test data incomplete - needs valid TOML structure"]
929    fn test_parse_valid_toml() -> Result<()> {
930        let toml_content = r#"
931name = "test_example"
932
933[[scenarios]]
934name = "basic_test"
935steps = [
936    { name = "setup", cmd = ["echo", "Setting up"] },
937    { name = "test", cmd = ["echo", "Testing"] }
938]
939
940[policy]
941security_level = "medium"
942max_execution_time = 300
943"#;
944
945        let config = parse_toml_config(toml_content)?;
946        assert_eq!(config.test.metadata.name, "test_example");
947        assert_eq!(config.steps.len(), 2);
948        Ok(())
949    }
950
951    #[test]
952    fn test_validate_config() {
953        let config = TestConfig {
954            test: TestMetadataSection {
955                metadata: TestMetadata {
956                    name: "test".to_string(),
957                    description: Some("test description".to_string()),
958                    timeout: None,
959                },
960            },
961            services: None,
962            steps: vec![StepConfig {
963                name: "step".to_string(),
964                command: vec!["echo".to_string(), "test".to_string()],
965                service: None,
966                expected_output_regex: None,
967                workdir: None,
968                env: None,
969                expected_exit_code: None,
970                continue_on_failure: None,
971            }],
972            assertions: None,
973            otel_validation: None,
974        };
975
976        assert!(config.validate().is_ok());
977    }
978
979    #[test]
980    fn test_validate_empty_name() {
981        let config = TestConfig {
982            test: TestMetadataSection {
983                metadata: TestMetadata {
984                    name: "".to_string(),
985                    description: Some("test description".to_string()),
986                    timeout: None,
987                },
988            },
989            steps: vec![],
990            services: None,
991            assertions: None,
992            otel_validation: None,
993        };
994
995        assert!(config.validate().is_err());
996    }
997
998    #[test]
999    fn test_validate_empty_scenarios() {
1000        let config = TestConfig {
1001            test: TestMetadataSection {
1002                metadata: TestMetadata {
1003                    name: "test".to_string(),
1004                    description: Some("test description".to_string()),
1005                    timeout: None,
1006                },
1007            },
1008            steps: vec![],
1009            services: None,
1010            assertions: None,
1011            otel_validation: None,
1012        };
1013
1014        assert!(config.validate().is_err());
1015    }
1016}