1use crate::error::{CleanroomError, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::Path;
10use std::time::Duration;
11
12#[derive(Debug, Deserialize, Serialize, Clone)]
14pub struct TestConfig {
15 pub test: TestMetadataSection,
17 pub services: Option<HashMap<String, ServiceConfig>>,
19 pub steps: Vec<StepConfig>,
21 pub assertions: Option<HashMap<String, serde_json::Value>>,
23 pub otel_validation: Option<OtelValidationSection>,
25}
26
27#[derive(Debug, Deserialize, Serialize, Clone)]
29pub struct TestMetadataSection {
30 pub metadata: TestMetadata,
32}
33
34#[derive(Debug, Deserialize, Serialize, Clone)]
36pub struct TestMetadata {
37 pub name: String,
39 pub description: Option<String>,
41 pub timeout: Option<String>,
43}
44
45#[derive(Debug, Deserialize, Serialize, Clone)]
47pub struct ScenarioConfig {
48 pub name: String,
50 pub steps: Vec<StepConfig>,
52 pub concurrent: Option<bool>,
54 pub timeout_ms: Option<u64>,
56 pub policy: Option<PolicyConfig>,
58}
59
60#[derive(Debug, Deserialize, Serialize, Clone)]
62pub struct StepConfig {
63 pub name: String,
65 pub command: Vec<String>,
67 pub expected_output_regex: Option<String>,
69 pub workdir: Option<String>,
71 pub env: Option<HashMap<String, String>>,
73 pub expected_exit_code: Option<i32>,
75 pub continue_on_failure: Option<bool>,
77 pub service: Option<String>,
79}
80
81#[derive(Debug, Deserialize, Serialize, Clone)]
83pub struct PolicyConfig {
84 pub security_level: Option<String>,
86 pub max_execution_time: Option<u64>,
88 pub max_memory_mb: Option<u64>,
90 pub max_cpu_usage: Option<f64>,
92 pub allowed_network_hosts: Option<Vec<String>>,
94 pub disallowed_commands: Option<Vec<String>>,
96}
97
98#[derive(Debug, Deserialize, Serialize, Clone)]
100pub struct ServiceConfig {
101 pub r#type: String,
103 pub plugin: String,
105 pub image: Option<String>,
107 pub env: Option<HashMap<String, String>>,
109 pub ports: Option<Vec<u16>>,
111 pub volumes: Option<Vec<VolumeConfig>>,
113 pub health_check: Option<HealthCheckConfig>,
115 pub username: Option<String>,
117 pub password: Option<String>,
119 pub strict: Option<bool>,
121}
122
123#[derive(Debug, Deserialize, Serialize, Clone)]
125pub struct VolumeConfig {
126 pub host_path: String,
128 pub container_path: String,
130 pub read_only: Option<bool>,
132}
133
134impl VolumeConfig {
135 pub fn validate(&self) -> Result<()> {
144 use std::path::Path;
145
146 if self.host_path.trim().is_empty() {
148 return Err(CleanroomError::validation_error(
149 "Volume host path cannot be empty",
150 ));
151 }
152
153 if self.container_path.trim().is_empty() {
155 return Err(CleanroomError::validation_error(
156 "Volume container path cannot be empty",
157 ));
158 }
159
160 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 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 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#[derive(Debug, Deserialize, Serialize, Clone)]
193pub struct HealthCheckConfig {
194 pub cmd: Vec<String>,
196 pub interval: Option<u64>,
198 pub timeout: Option<u64>,
200 pub retries: Option<u32>,
202}
203
204#[derive(Debug, Deserialize, Serialize, Clone)]
206pub struct TimeoutConfig {
207 pub step_timeout_ms: Option<u64>,
209 pub scenario_timeout_ms: Option<u64>,
211 pub test_timeout_ms: Option<u64>,
213}
214
215#[derive(Debug, Deserialize, Serialize, Clone)]
218pub struct CleanroomConfig {
219 pub project: ProjectConfig,
221 pub cli: CliConfig,
223 pub containers: ContainerConfig,
225 pub services: ServiceDefaultsConfig,
227 pub observability: ObservabilityConfig,
229 pub plugins: PluginConfig,
231 pub performance: PerformanceConfig,
233 pub test_execution: TestExecutionConfig,
235 pub reporting: ReportingConfig,
237 pub security: SecurityConfig,
239}
240
241#[derive(Debug, Deserialize, Serialize, Clone)]
243pub struct ProjectConfig {
244 pub name: String,
246 pub version: Option<String>,
248 pub description: Option<String>,
250}
251
252#[derive(Debug, Deserialize, Serialize, Clone)]
254pub struct CliConfig {
255 pub parallel: bool,
257 pub jobs: usize,
259 pub output_format: String,
261 pub fail_fast: bool,
263 pub watch: bool,
265 pub interactive: bool,
267}
268
269#[derive(Debug, Deserialize, Serialize, Clone)]
271pub struct ContainerConfig {
272 pub reuse_enabled: bool,
274 pub default_image: String,
276 pub pull_policy: String,
278 pub cleanup_policy: String,
280 pub max_containers: usize,
282 pub startup_timeout: Duration,
284}
285
286#[derive(Debug, Deserialize, Serialize, Clone)]
288pub struct ServiceDefaultsConfig {
289 pub default_timeout: Duration,
291 pub health_check_interval: Duration,
293 pub health_check_timeout: Duration,
295 pub max_retries: u32,
297}
298
299#[derive(Debug, Deserialize, Serialize, Clone)]
301pub struct ObservabilityConfig {
302 pub enable_tracing: bool,
304 pub enable_metrics: bool,
306 pub enable_logging: bool,
308 pub log_level: String,
310 pub metrics_port: u16,
312 pub traces_endpoint: Option<String>,
314}
315
316#[derive(Debug, Deserialize, Serialize, Clone)]
318pub struct PluginConfig {
319 pub auto_discover: bool,
321 pub plugin_dir: String,
323 pub enabled_plugins: Vec<String>,
325}
326
327#[derive(Debug, Deserialize, Serialize, Clone)]
329pub struct PerformanceConfig {
330 pub container_pool_size: usize,
332 pub lazy_initialization: bool,
334 pub cache_compiled_tests: bool,
336 pub parallel_step_execution: bool,
338}
339
340#[derive(Debug, Deserialize, Serialize, Clone)]
342pub struct TestExecutionConfig {
343 pub default_timeout: Duration,
345 pub step_timeout: Duration,
347 pub retry_on_failure: bool,
349 pub retry_count: u32,
351 pub test_dir: String,
353}
354
355#[derive(Debug, Deserialize, Serialize, Clone)]
357pub struct ReportingConfig {
358 pub generate_html: bool,
360 pub generate_junit: bool,
362 pub report_dir: String,
364 pub include_timestamps: bool,
366 pub include_logs: bool,
368}
369
370#[derive(Debug, Deserialize, Serialize, Clone)]
372pub struct SecurityConfig {
373 pub hermetic_isolation: bool,
375 pub network_isolation: bool,
377 pub file_system_isolation: bool,
379 pub security_level: String,
381}
382
383#[derive(Debug, Deserialize, Serialize, Clone)]
385pub struct OtelValidationSection {
386 pub enabled: bool,
388 pub validate_spans: Option<bool>,
390 pub validate_traces: Option<bool>,
392 pub validate_exports: Option<bool>,
394 pub validate_performance: Option<bool>,
396 pub max_overhead_ms: Option<f64>,
398 pub expected_spans: Option<Vec<ExpectedSpanConfig>>,
400 pub expected_traces: Option<Vec<ExpectedTraceConfig>>,
402}
403
404#[derive(Debug, Deserialize, Serialize, Clone)]
406pub struct ExpectedSpanConfig {
407 pub name: String,
409 pub attributes: Option<HashMap<String, String>>,
411 pub required: Option<bool>,
413 pub min_duration_ms: Option<f64>,
415 pub max_duration_ms: Option<f64>,
417}
418
419#[derive(Debug, Deserialize, Serialize, Clone)]
421pub struct ExpectedTraceConfig {
422 pub trace_id: Option<String>,
424 pub span_names: Vec<String>,
426 pub complete: Option<bool>,
428 pub parent_child: Option<Vec<(String, String)>>,
430}
431
432impl TestConfig {
433 pub fn validate(&self) -> Result<()> {
435 if self.test.metadata.name.trim().is_empty() {
437 return Err(CleanroomError::validation_error(
438 "Test name cannot be empty",
439 ));
440 }
441
442 if self.steps.is_empty() {
444 return Err(CleanroomError::validation_error(
445 "At least one step is required",
446 ));
447 }
448
449 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 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 if let Some(assertions) = &self.assertions {
466 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 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 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 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 return Err(CleanroomError::validation_error(
545 "Service image is required for container-based services",
546 ));
547 }
548
549 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 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
588pub 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
594pub 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
605impl 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
683impl CleanroomConfig {
685 pub fn validate(&self) -> Result<()> {
687 if self.project.name.trim().is_empty() {
689 return Err(CleanroomError::validation_error(
690 "Project name cannot be empty",
691 ));
692 }
693
694 if self.cli.jobs == 0 {
696 return Err(CleanroomError::validation_error(
697 "CLI jobs must be greater than 0",
698 ));
699 }
700
701 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 if self.observability.metrics_port == 0 {
716 return Err(CleanroomError::validation_error(
717 "Metrics port must be greater than 0",
718 ));
719 }
720
721 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 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
745pub fn load_cleanroom_config() -> Result<CleanroomConfig> {
747 let mut config = CleanroomConfig::default();
748
749 config = apply_env_overrides(config)?;
751
752 if let Ok(project_config) = load_cleanroom_config_from_file("cleanroom.toml") {
754 config = merge_configs(config, project_config);
755 }
756
757 if let Ok(user_config) = load_cleanroom_config_from_user_dir() {
759 config = merge_configs(config, user_config);
760 }
761
762 config.validate()?;
764
765 Ok(config)
766}
767
768pub 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
783fn 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
801fn apply_env_overrides(mut config: CleanroomConfig) -> Result<CleanroomConfig> {
803 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 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 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 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
880fn merge_configs(mut base: CleanroomConfig, override_config: CleanroomConfig) -> CleanroomConfig {
882 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 base.cli = override_config.cli;
895
896 base.containers = override_config.containers;
898
899 base.services = override_config.services;
901
902 base.observability = override_config.observability;
904
905 base.plugins = override_config.plugins;
907
908 base.performance = override_config.performance;
910
911 base.test_execution = override_config.test_execution;
913
914 base.reporting = override_config.reporting;
916
917 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}