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