1use 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#[derive(Debug, Deserialize, Serialize, Clone)]
15pub struct TestConfig {
16 pub name: String,
18 pub scenarios: Vec<ScenarioConfig>,
20 pub policy: Option<PolicyConfig>,
22 pub services: Option<Vec<ServiceConfig>>,
24 pub env: Option<HashMap<String, String>>,
26 pub timeout: Option<TimeoutConfig>,
28}
29
30#[derive(Debug, Deserialize, Serialize, Clone)]
32pub struct ScenarioConfig {
33 pub name: String,
35 pub steps: Vec<StepConfig>,
37 pub concurrent: Option<bool>,
39 pub timeout_ms: Option<u64>,
41 pub policy: Option<PolicyConfig>,
43}
44
45#[derive(Debug, Deserialize, Serialize, Clone)]
47pub struct StepConfig {
48 pub name: String,
50 pub cmd: Vec<String>,
52 pub workdir: Option<String>,
54 pub env: Option<HashMap<String, String>>,
56 pub expected_exit_code: Option<i32>,
58 pub continue_on_failure: Option<bool>,
60}
61
62#[derive(Debug, Deserialize, Serialize, Clone)]
64pub struct PolicyConfig {
65 pub security_level: Option<String>,
67 pub max_execution_time: Option<u64>,
69 pub max_memory_mb: Option<u64>,
71 pub max_cpu_usage: Option<f64>,
73 pub allowed_network_hosts: Option<Vec<String>>,
75 pub disallowed_commands: Option<Vec<String>>,
77}
78
79#[derive(Debug, Deserialize, Serialize, Clone)]
81pub struct ServiceConfig {
82 pub name: String,
84 pub service_type: String,
86 pub image: String,
88 pub env: Option<HashMap<String, String>>,
90 pub ports: Option<Vec<u16>>,
92 pub volumes: Option<Vec<VolumeConfig>>,
94 pub health_check: Option<HealthCheckConfig>,
96}
97
98#[derive(Debug, Deserialize, Serialize, Clone)]
100pub struct VolumeConfig {
101 pub host_path: String,
103 pub container_path: String,
105 pub read_only: Option<bool>,
107}
108
109#[derive(Debug, Deserialize, Serialize, Clone)]
111pub struct HealthCheckConfig {
112 pub cmd: Vec<String>,
114 pub interval: Option<u64>,
116 pub timeout: Option<u64>,
118 pub retries: Option<u32>,
120}
121
122#[derive(Debug, Deserialize, Serialize, Clone)]
124pub struct TimeoutConfig {
125 pub step_timeout_ms: Option<u64>,
127 pub scenario_timeout_ms: Option<u64>,
129 pub test_timeout_ms: Option<u64>,
131}
132
133#[derive(Debug, Deserialize, Serialize, Clone)]
136pub struct CleanroomConfig {
137 pub project: ProjectConfig,
139 pub cli: CliConfig,
141 pub containers: ContainerConfig,
143 pub services: ServiceDefaultsConfig,
145 pub observability: ObservabilityConfig,
147 pub plugins: PluginConfig,
149 pub performance: PerformanceConfig,
151 pub test_execution: TestExecutionConfig,
153 pub reporting: ReportingConfig,
155 pub security: SecurityConfig,
157}
158
159#[derive(Debug, Deserialize, Serialize, Clone)]
161pub struct ProjectConfig {
162 pub name: String,
164 pub version: Option<String>,
166 pub description: Option<String>,
168}
169
170#[derive(Debug, Deserialize, Serialize, Clone)]
172pub struct CliConfig {
173 pub parallel: bool,
175 pub jobs: usize,
177 pub output_format: String,
179 pub fail_fast: bool,
181 pub watch: bool,
183 pub interactive: bool,
185}
186
187#[derive(Debug, Deserialize, Serialize, Clone)]
189pub struct ContainerConfig {
190 pub reuse_enabled: bool,
192 pub default_image: String,
194 pub pull_policy: String,
196 pub cleanup_policy: String,
198 pub max_containers: usize,
200 pub startup_timeout: Duration,
202}
203
204#[derive(Debug, Deserialize, Serialize, Clone)]
206pub struct ServiceDefaultsConfig {
207 pub default_timeout: Duration,
209 pub health_check_interval: Duration,
211 pub health_check_timeout: Duration,
213 pub max_retries: u32,
215}
216
217#[derive(Debug, Deserialize, Serialize, Clone)]
219pub struct ObservabilityConfig {
220 pub enable_tracing: bool,
222 pub enable_metrics: bool,
224 pub enable_logging: bool,
226 pub log_level: String,
228 pub metrics_port: u16,
230 pub traces_endpoint: Option<String>,
232}
233
234#[derive(Debug, Deserialize, Serialize, Clone)]
236pub struct PluginConfig {
237 pub auto_discover: bool,
239 pub plugin_dir: String,
241 pub enabled_plugins: Vec<String>,
243}
244
245#[derive(Debug, Deserialize, Serialize, Clone)]
247pub struct PerformanceConfig {
248 pub container_pool_size: usize,
250 pub lazy_initialization: bool,
252 pub cache_compiled_tests: bool,
254 pub parallel_step_execution: bool,
256}
257
258#[derive(Debug, Deserialize, Serialize, Clone)]
260pub struct TestExecutionConfig {
261 pub default_timeout: Duration,
263 pub step_timeout: Duration,
265 pub retry_on_failure: bool,
267 pub retry_count: u32,
269 pub test_dir: String,
271}
272
273#[derive(Debug, Deserialize, Serialize, Clone)]
275pub struct ReportingConfig {
276 pub generate_html: bool,
278 pub generate_junit: bool,
280 pub report_dir: String,
282 pub include_timestamps: bool,
284 pub include_logs: bool,
286}
287
288#[derive(Debug, Deserialize, Serialize, Clone)]
290pub struct SecurityConfig {
291 pub hermetic_isolation: bool,
293 pub network_isolation: bool,
295 pub file_system_isolation: bool,
297 pub security_level: String,
299}
300
301impl TestConfig {
302 pub fn validate(&self) -> Result<()> {
304 if self.name.trim().is_empty() {
306 return Err(CleanroomError::validation_error("Test name cannot be empty"));
307 }
308
309 if self.scenarios.is_empty() {
311 return Err(CleanroomError::validation_error("At least one scenario is required"));
312 }
313
314 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 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 if let Some(policy) = &self.policy {
330 policy.validate()?;
331 }
332
333 Ok(())
334 }
335
336 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 _ => {} }
348 }
349
350 policy
354 } else {
355 Policy::default()
356 }
357 }
358}
359
360impl ScenarioConfig {
361 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 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 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 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
438pub 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
444pub 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
455impl 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
529impl CleanroomConfig {
531 pub fn validate(&self) -> Result<()> {
533 if self.project.name.trim().is_empty() {
535 return Err(CleanroomError::validation_error("Project name cannot be empty"));
536 }
537
538 if self.cli.jobs == 0 {
540 return Err(CleanroomError::validation_error("CLI jobs must be greater than 0"));
541 }
542
543 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 if self.observability.metrics_port == 0 {
554 return Err(CleanroomError::validation_error("Metrics port must be greater than 0"));
555 }
556
557 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 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
577pub fn load_cleanroom_config() -> Result<CleanroomConfig> {
579 let mut config = CleanroomConfig::default();
580
581 config = apply_env_overrides(config)?;
583
584 if let Ok(project_config) = load_cleanroom_config_from_file("cleanroom.toml") {
586 config = merge_configs(config, project_config);
587 }
588
589 if let Ok(user_config) = load_cleanroom_config_from_user_dir() {
591 config = merge_configs(config, user_config);
592 }
593
594 config.validate()?;
596
597 Ok(config)
598}
599
600pub 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
613fn 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
626fn apply_env_overrides(mut config: CleanroomConfig) -> Result<CleanroomConfig> {
628 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 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 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 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
687fn merge_configs(mut base: CleanroomConfig, override_config: CleanroomConfig) -> CleanroomConfig {
689 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 base.cli = override_config.cli;
702
703 base.containers = override_config.containers;
705
706 base.services = override_config.services;
708
709 base.observability = override_config.observability;
711
712 base.plugins = override_config.plugins;
714
715 base.performance = override_config.performance;
717
718 base.test_execution = override_config.test_execution;
720
721 base.reporting = override_config.reporting;
723
724 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}