1pub mod constant;
15pub mod csv_header;
16pub mod csv_replay;
17pub mod histogram;
18pub mod jitter;
19pub mod log_replay;
20pub mod log_template;
21pub mod sawtooth;
22pub mod sequence;
23pub mod sine;
24pub mod spike;
25pub mod step;
26pub mod summary;
27pub mod uniform;
28
29pub use self::jitter::JitterWrapper;
30
31use std::collections::{BTreeMap, HashMap};
32
33use self::constant::Constant;
34use self::csv_replay::CsvReplayGenerator;
35use self::log_replay::LogReplayGenerator;
36use self::log_template::{LogTemplateGenerator, TemplateEntry};
37use self::sawtooth::Sawtooth;
38use self::sequence::SequenceGenerator;
39use self::sine::Sine;
40use self::spike::SpikeGenerator;
41use self::step::StepGenerator;
42use self::uniform::UniformRandom;
43use crate::model::log::{LogEvent, Severity};
44use crate::{ConfigError, SondaError};
45
46pub trait ValueGenerator: Send + Sync {
51 fn value(&self, tick: u64) -> f64;
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
72#[cfg_attr(feature = "config", derive(serde::Serialize, serde::Deserialize))]
73pub struct CsvColumnSpec {
74 pub index: usize,
76 pub name: String,
78 #[cfg_attr(feature = "config", serde(default))]
81 pub labels: Option<HashMap<String, String>>,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97#[cfg_attr(
98 feature = "config",
99 derive(serde::Serialize, serde::Deserialize),
100 serde(rename_all = "snake_case")
101)]
102pub enum FlapEnum {
103 Boolean,
104 LinkState,
105 OperState,
106 AdminState,
107 NeighborState,
108}
109
110impl FlapEnum {
111 pub fn defaults(self) -> (f64, f64) {
113 match self {
114 FlapEnum::Boolean | FlapEnum::LinkState => (1.0, 0.0),
115 FlapEnum::OperState | FlapEnum::AdminState => (1.0, 2.0),
116 FlapEnum::NeighborState => (6.0, 1.0),
117 }
118 }
119}
120
121#[derive(Debug, Clone)]
164#[cfg_attr(feature = "config", derive(serde::Serialize, serde::Deserialize))]
165#[cfg_attr(feature = "config", serde(tag = "type"))]
166#[non_exhaustive]
167pub enum GeneratorConfig {
168 #[cfg_attr(feature = "config", serde(rename = "constant"))]
170 Constant {
171 value: f64,
173 },
174 #[cfg_attr(feature = "config", serde(rename = "uniform"))]
176 Uniform {
177 min: f64,
179 max: f64,
181 seed: Option<u64>,
183 },
184 #[cfg_attr(feature = "config", serde(rename = "sine"))]
186 Sine {
187 amplitude: f64,
189 period_secs: f64,
191 offset: f64,
193 },
194 #[cfg_attr(feature = "config", serde(rename = "sawtooth"))]
196 Sawtooth {
197 min: f64,
199 max: f64,
201 period_secs: f64,
203 },
204 #[cfg_attr(feature = "config", serde(rename = "sequence"))]
206 Sequence {
207 values: Vec<f64>,
209 repeat: Option<bool>,
212 },
213 #[cfg_attr(feature = "config", serde(rename = "spike"))]
215 Spike {
216 baseline: f64,
218 magnitude: f64,
220 duration_secs: f64,
222 interval_secs: f64,
224 },
225 #[cfg_attr(feature = "config", serde(rename = "csv_replay"))]
227 CsvReplay {
228 file: String,
230 #[cfg_attr(feature = "config", serde(skip))]
235 column: Option<usize>,
236 #[cfg_attr(feature = "config", serde(default))]
243 columns: Option<Vec<CsvColumnSpec>>,
244 #[cfg_attr(feature = "config", serde(default))]
247 repeat: Option<bool>,
248 },
249 #[cfg_attr(feature = "config", serde(rename = "step"))]
253 Step {
254 #[cfg_attr(feature = "config", serde(default))]
256 start: Option<f64>,
257 step_size: f64,
259 max: Option<f64>,
262 },
263
264 #[cfg_attr(feature = "config", serde(rename = "flap"))]
290 Flap {
291 #[cfg_attr(feature = "config", serde(default))]
294 up_duration: Option<String>,
295 #[cfg_attr(feature = "config", serde(default))]
298 down_duration: Option<String>,
299 #[cfg_attr(feature = "config", serde(default))]
301 up_value: Option<f64>,
302 #[cfg_attr(feature = "config", serde(default))]
304 down_value: Option<f64>,
305 #[cfg_attr(
309 feature = "config",
310 serde(default, rename = "enum", skip_serializing_if = "Option::is_none")
311 )]
312 enum_kind: Option<FlapEnum>,
313 },
314
315 #[cfg_attr(feature = "config", serde(rename = "saturation"))]
341 Saturation {
342 #[cfg_attr(feature = "config", serde(default))]
344 baseline: Option<f64>,
345 #[cfg_attr(feature = "config", serde(default))]
347 ceiling: Option<f64>,
348 #[cfg_attr(feature = "config", serde(default))]
350 time_to_saturate: Option<String>,
351 },
352
353 #[cfg_attr(feature = "config", serde(rename = "leak"))]
381 Leak {
382 #[cfg_attr(feature = "config", serde(default))]
384 baseline: Option<f64>,
385 #[cfg_attr(feature = "config", serde(default))]
387 ceiling: Option<f64>,
388 #[cfg_attr(feature = "config", serde(default))]
392 time_to_ceiling: Option<String>,
393 },
394
395 #[cfg_attr(feature = "config", serde(rename = "degradation"))]
412 Degradation {
413 #[cfg_attr(feature = "config", serde(default))]
415 baseline: Option<f64>,
416 #[cfg_attr(feature = "config", serde(default))]
418 ceiling: Option<f64>,
419 #[cfg_attr(feature = "config", serde(default))]
421 time_to_degrade: Option<String>,
422 #[cfg_attr(feature = "config", serde(default))]
424 noise: Option<f64>,
425 #[cfg_attr(feature = "config", serde(default))]
427 noise_seed: Option<u64>,
428 },
429
430 #[cfg_attr(feature = "config", serde(rename = "steady"))]
447 Steady {
448 #[cfg_attr(feature = "config", serde(default))]
450 center: Option<f64>,
451 #[cfg_attr(feature = "config", serde(default))]
453 amplitude: Option<f64>,
454 #[cfg_attr(feature = "config", serde(default))]
456 period: Option<String>,
457 #[cfg_attr(feature = "config", serde(default))]
459 noise: Option<f64>,
460 #[cfg_attr(feature = "config", serde(default))]
462 noise_seed: Option<u64>,
463 },
464
465 #[cfg_attr(feature = "config", serde(rename = "spike_event"))]
481 SpikeEvent {
482 #[cfg_attr(feature = "config", serde(default))]
484 baseline: Option<f64>,
485 #[cfg_attr(feature = "config", serde(default))]
487 spike_height: Option<f64>,
488 #[cfg_attr(feature = "config", serde(default))]
490 spike_duration: Option<String>,
491 #[cfg_attr(feature = "config", serde(default))]
493 spike_interval: Option<String>,
494 },
495}
496
497impl GeneratorConfig {
498 pub fn is_alias(&self) -> bool {
501 matches!(
502 self,
503 GeneratorConfig::Flap { .. }
504 | GeneratorConfig::Saturation { .. }
505 | GeneratorConfig::Leak { .. }
506 | GeneratorConfig::Degradation { .. }
507 | GeneratorConfig::Steady { .. }
508 | GeneratorConfig::SpikeEvent { .. }
509 )
510 }
511}
512
513pub fn create_generator(
527 config: &GeneratorConfig,
528 rate: f64,
529) -> Result<Box<dyn ValueGenerator>, SondaError> {
530 match config {
531 GeneratorConfig::Constant { value } => Ok(Box::new(Constant::new(*value))),
532 GeneratorConfig::Uniform { min, max, seed } => {
533 Ok(Box::new(UniformRandom::new(*min, *max, seed.unwrap_or(0))))
534 }
535 GeneratorConfig::Sine {
536 amplitude,
537 period_secs,
538 offset,
539 } => Ok(Box::new(Sine::new(*amplitude, *period_secs, *offset, rate))),
540 GeneratorConfig::Sawtooth {
541 min,
542 max,
543 period_secs,
544 } => Ok(Box::new(Sawtooth::new(*min, *max, *period_secs, rate))),
545 GeneratorConfig::Spike {
546 baseline,
547 magnitude,
548 duration_secs,
549 interval_secs,
550 } => {
551 if *interval_secs <= 0.0 {
552 return Err(SondaError::Config(ConfigError::invalid(
553 "spike generator requires interval_secs > 0",
554 )));
555 }
556 if *duration_secs < 0.0 {
557 return Err(SondaError::Config(ConfigError::invalid(
558 "spike generator requires duration_secs >= 0",
559 )));
560 }
561 Ok(Box::new(SpikeGenerator::new(
562 *baseline,
563 *magnitude,
564 *duration_secs,
565 *interval_secs,
566 rate,
567 )))
568 }
569 GeneratorConfig::Sequence { values, repeat } => Ok(Box::new(SequenceGenerator::new(
570 values.clone(),
571 repeat.unwrap_or(true),
572 )?)),
573 GeneratorConfig::CsvReplay {
574 file,
575 column,
576 repeat,
577 columns,
578 } => {
579 if columns.is_some() {
580 return Err(SondaError::Config(ConfigError::invalid(
581 "csv_replay: call expand_scenario before create_generator when 'columns' is set",
582 )));
583 }
584 Ok(Box::new(CsvReplayGenerator::new(
585 file,
586 column.unwrap_or(0),
587 repeat.unwrap_or(true),
588 )?))
589 }
590 GeneratorConfig::Step {
591 start,
592 step_size,
593 max,
594 } => Ok(Box::new(StepGenerator::new(
595 start.unwrap_or(0.0),
596 *step_size,
597 *max,
598 ))),
599 GeneratorConfig::Flap { .. }
602 | GeneratorConfig::Saturation { .. }
603 | GeneratorConfig::Leak { .. }
604 | GeneratorConfig::Degradation { .. }
605 | GeneratorConfig::Steady { .. }
606 | GeneratorConfig::SpikeEvent { .. } => Err(SondaError::Config(ConfigError::invalid(
607 "operational alias generator must be desugared via \
608 desugar_entry() or desugar_scenario_config() before calling create_generator()",
609 ))),
610 }
611}
612
613pub fn wrap_with_jitter(
626 generator: Box<dyn ValueGenerator>,
627 jitter: Option<f64>,
628 jitter_seed: Option<u64>,
629) -> Box<dyn ValueGenerator> {
630 match jitter {
631 Some(j) if j != 0.0 => Box::new(JitterWrapper::new(generator, j, jitter_seed.unwrap_or(0))),
632 _ => generator,
633 }
634}
635
636pub trait LogGenerator: Send + Sync {
645 fn generate(&self, tick: u64) -> LogEvent;
647}
648
649#[derive(Debug, Clone)]
667#[cfg_attr(feature = "config", derive(serde::Serialize, serde::Deserialize))]
668pub struct TemplateConfig {
669 pub message: String,
671 #[cfg_attr(feature = "config", serde(default))]
676 pub field_pools: BTreeMap<String, Vec<String>>,
677}
678
679#[derive(Debug, Clone)]
708#[cfg_attr(feature = "config", derive(serde::Serialize, serde::Deserialize))]
709#[cfg_attr(feature = "config", serde(tag = "type"))]
710pub enum LogGeneratorConfig {
711 #[cfg_attr(feature = "config", serde(rename = "template"))]
713 Template {
714 templates: Vec<TemplateConfig>,
716 #[cfg_attr(feature = "config", serde(default))]
719 severity_weights: Option<HashMap<String, f64>>,
720 seed: Option<u64>,
722 },
723 #[cfg_attr(feature = "config", serde(rename = "replay"))]
725 Replay {
726 file: String,
728 },
729}
730
731fn parse_severity(s: &str) -> Result<Severity, SondaError> {
733 match s.to_lowercase().as_str() {
734 "trace" => Ok(Severity::Trace),
735 "debug" => Ok(Severity::Debug),
736 "info" => Ok(Severity::Info),
737 "warn" | "warning" => Ok(Severity::Warn),
738 "error" => Ok(Severity::Error),
739 "fatal" => Ok(Severity::Fatal),
740 other => Err(SondaError::Config(ConfigError::invalid(format!(
741 "unknown severity {:?}: must be one of trace, debug, info, warn, error, fatal",
742 other
743 )))),
744 }
745}
746
747pub fn create_log_generator(
754 config: &LogGeneratorConfig,
755) -> Result<Box<dyn LogGenerator>, SondaError> {
756 match config {
757 LogGeneratorConfig::Template {
758 templates,
759 severity_weights,
760 seed,
761 } => {
762 let seed = seed.unwrap_or(0);
763
764 let weights: Vec<(Severity, f64)> = if let Some(map) = severity_weights {
766 let mut pairs = Vec::with_capacity(map.len());
767 for (name, weight) in map {
768 let severity = parse_severity(name)?;
769 pairs.push((severity, *weight));
770 }
771 pairs.sort_by_key(|a| a.0);
773 pairs
774 } else {
775 vec![]
776 };
777
778 let entries: Vec<TemplateEntry> = templates
780 .iter()
781 .map(|tc| TemplateEntry {
782 message: tc.message.clone(),
783 field_pools: tc.field_pools.clone(),
784 })
785 .collect();
786
787 Ok(Box::new(LogTemplateGenerator::new(entries, weights, seed)))
788 }
789 LogGeneratorConfig::Replay { file } => {
790 let path = std::path::Path::new(file);
791 Ok(Box::new(LogReplayGenerator::from_file(path)?))
792 }
793 }
794}
795
796#[cfg(test)]
797mod tests {
798 use super::*;
799
800 #[test]
803 fn factory_constant_returns_configured_value() {
804 let config = GeneratorConfig::Constant { value: 1.0 };
805 let gen = create_generator(&config, 100.0).expect("constant factory");
806 assert_eq!(gen.value(0), 1.0);
807 assert_eq!(gen.value(1_000_000), 1.0);
808 }
809
810 #[test]
811 fn factory_uniform_returns_values_in_range() {
812 let config = GeneratorConfig::Uniform {
813 min: 0.0,
814 max: 1.0,
815 seed: Some(7),
816 };
817 let gen = create_generator(&config, 100.0).expect("uniform factory");
818 for tick in 0..1000 {
819 let v = gen.value(tick);
820 assert!(
821 v >= 0.0 && v <= 1.0,
822 "uniform value {v} out of [0,1] at tick {tick}"
823 );
824 }
825 }
826
827 #[test]
828 fn factory_uniform_seed_none_defaults_to_zero_seed() {
829 let config_none = GeneratorConfig::Uniform {
831 min: 0.0,
832 max: 1.0,
833 seed: None,
834 };
835 let config_zero = GeneratorConfig::Uniform {
836 min: 0.0,
837 max: 1.0,
838 seed: Some(0),
839 };
840 let gen_none = create_generator(&config_none, 1.0).expect("uniform none factory");
841 let gen_zero = create_generator(&config_zero, 1.0).expect("uniform zero factory");
842 for tick in 0..100 {
843 assert_eq!(
844 gen_none.value(tick),
845 gen_zero.value(tick),
846 "seed=None must equal seed=Some(0) at tick {tick}"
847 );
848 }
849 }
850
851 #[test]
852 fn factory_sine_value_at_zero_equals_offset() {
853 let config = GeneratorConfig::Sine {
854 amplitude: 5.0,
855 period_secs: 10.0,
856 offset: 3.0,
857 };
858 let gen = create_generator(&config, 1.0).expect("sine factory");
859 assert!(
860 (gen.value(0) - 3.0).abs() < 1e-10,
861 "sine factory: value(0) must equal offset"
862 );
863 }
864
865 #[test]
866 fn factory_sawtooth_value_at_zero_equals_min() {
867 let config = GeneratorConfig::Sawtooth {
868 min: 5.0,
869 max: 15.0,
870 period_secs: 10.0,
871 };
872 let gen = create_generator(&config, 1.0).expect("sawtooth factory");
873 assert_eq!(
874 gen.value(0),
875 5.0,
876 "sawtooth factory: value(0) must equal min"
877 );
878 }
879
880 #[test]
883 fn factory_sequence_repeat_true_creates_working_generator() {
884 let config = GeneratorConfig::Sequence {
885 values: vec![1.0, 2.0, 3.0],
886 repeat: Some(true),
887 };
888 let gen = create_generator(&config, 1.0).expect("sequence factory repeat=true");
889 assert_eq!(gen.value(0), 1.0);
890 assert_eq!(gen.value(1), 2.0);
891 assert_eq!(gen.value(2), 3.0);
892 assert_eq!(gen.value(3), 1.0, "should wrap around");
893 }
894
895 #[test]
896 fn factory_sequence_repeat_false_creates_working_generator() {
897 let config = GeneratorConfig::Sequence {
898 values: vec![1.0, 2.0, 3.0],
899 repeat: Some(false),
900 };
901 let gen = create_generator(&config, 1.0).expect("sequence factory repeat=false");
902 assert_eq!(gen.value(0), 1.0);
903 assert_eq!(gen.value(4), 3.0, "should clamp to last value");
904 }
905
906 #[test]
907 fn factory_sequence_repeat_none_defaults_to_true() {
908 let config = GeneratorConfig::Sequence {
909 values: vec![1.0, 2.0],
910 repeat: None,
911 };
912 let gen = create_generator(&config, 1.0).expect("sequence factory repeat=None");
913 assert_eq!(
915 gen.value(2),
916 1.0,
917 "repeat=None should default to true (cycling)"
918 );
919 }
920
921 #[test]
922 fn factory_sequence_empty_values_returns_error() {
923 let config = GeneratorConfig::Sequence {
924 values: vec![],
925 repeat: Some(true),
926 };
927 let result = create_generator(&config, 1.0);
928 assert!(result.is_err(), "empty sequence must return an error");
929 }
930
931 #[test]
934 fn factory_step_linear_growth() {
935 let config = GeneratorConfig::Step {
936 start: None,
937 step_size: 1.0,
938 max: None,
939 };
940 let gen = create_generator(&config, 1.0).expect("step factory");
941 assert_eq!(gen.value(0), 0.0);
942 assert_eq!(gen.value(1), 1.0);
943 assert_eq!(gen.value(100), 100.0);
944 }
945
946 #[test]
947 fn factory_step_with_start() {
948 let config = GeneratorConfig::Step {
949 start: Some(10.0),
950 step_size: 2.0,
951 max: None,
952 };
953 let gen = create_generator(&config, 1.0).expect("step factory with start");
954 assert_eq!(gen.value(0), 10.0);
955 assert_eq!(gen.value(1), 12.0);
956 assert_eq!(gen.value(5), 20.0);
957 }
958
959 #[test]
960 fn factory_step_with_wrap() {
961 let config = GeneratorConfig::Step {
962 start: Some(0.0),
963 step_size: 1.0,
964 max: Some(3.0),
965 };
966 let gen = create_generator(&config, 1.0).expect("step factory with wrap");
967 assert_eq!(gen.value(0), 0.0);
968 assert_eq!(gen.value(3), 0.0, "should wrap at max");
969 assert_eq!(gen.value(4), 1.0);
970 }
971
972 #[test]
973 fn factory_step_start_none_defaults_to_zero() {
974 let config_none = GeneratorConfig::Step {
975 start: None,
976 step_size: 1.0,
977 max: None,
978 };
979 let config_zero = GeneratorConfig::Step {
980 start: Some(0.0),
981 step_size: 1.0,
982 max: None,
983 };
984 let gen_none = create_generator(&config_none, 1.0).expect("step start=None");
985 let gen_zero = create_generator(&config_zero, 1.0).expect("step start=0");
986 for tick in 0..10 {
987 assert_eq!(
988 gen_none.value(tick),
989 gen_zero.value(tick),
990 "start=None must equal start=Some(0.0) at tick {tick}"
991 );
992 }
993 }
994
995 #[test]
998 fn factory_spike_returns_baseline_outside_window() {
999 let config = GeneratorConfig::Spike {
1000 baseline: 50.0,
1001 magnitude: 200.0,
1002 duration_secs: 10.0,
1003 interval_secs: 60.0,
1004 };
1005 let gen = create_generator(&config, 1.0).expect("spike factory");
1006 assert_eq!(gen.value(15), 50.0);
1008 }
1009
1010 #[test]
1011 fn factory_spike_returns_spike_inside_window() {
1012 let config = GeneratorConfig::Spike {
1013 baseline: 50.0,
1014 magnitude: 200.0,
1015 duration_secs: 10.0,
1016 interval_secs: 60.0,
1017 };
1018 let gen = create_generator(&config, 1.0).expect("spike factory");
1019 assert_eq!(gen.value(5), 250.0);
1021 }
1022
1023 #[test]
1024 fn factory_spike_zero_interval_returns_error() {
1025 let config = GeneratorConfig::Spike {
1026 baseline: 50.0,
1027 magnitude: 200.0,
1028 duration_secs: 10.0,
1029 interval_secs: 0.0,
1030 };
1031 let result = create_generator(&config, 1.0);
1032 assert!(result.is_err(), "interval_secs=0 must return an error");
1033 }
1034
1035 #[test]
1036 fn factory_spike_negative_interval_returns_error() {
1037 let config = GeneratorConfig::Spike {
1038 baseline: 50.0,
1039 magnitude: 200.0,
1040 duration_secs: 10.0,
1041 interval_secs: -1.0,
1042 };
1043 let result = create_generator(&config, 1.0);
1044 assert!(
1045 result.is_err(),
1046 "negative interval_secs must return an error"
1047 );
1048 }
1049
1050 #[test]
1051 fn factory_spike_negative_duration_returns_error() {
1052 let config = GeneratorConfig::Spike {
1053 baseline: 50.0,
1054 magnitude: 200.0,
1055 duration_secs: -5.0,
1056 interval_secs: 60.0,
1057 };
1058 let result = create_generator(&config, 1.0);
1059 assert!(
1060 result.is_err(),
1061 "negative duration_secs must return an error"
1062 );
1063 }
1064
1065 #[test]
1066 fn factory_spike_zero_duration_succeeds() {
1067 let config = GeneratorConfig::Spike {
1068 baseline: 50.0,
1069 magnitude: 200.0,
1070 duration_secs: 0.0,
1071 interval_secs: 60.0,
1072 };
1073 let gen = create_generator(&config, 1.0).expect("duration_secs=0 is valid");
1074 assert_eq!(gen.value(0), 50.0);
1076 assert_eq!(gen.value(30), 50.0);
1077 }
1078
1079 #[test]
1080 fn factory_csv_replay_with_columns_returns_error() {
1081 let config = GeneratorConfig::CsvReplay {
1082 file: "data.csv".to_string(),
1083 column: None,
1084 repeat: None,
1085 columns: Some(vec![CsvColumnSpec {
1086 index: 1,
1087 name: "cpu".to_string(),
1088 labels: None,
1089 }]),
1090 };
1091 let result = create_generator(&config, 1.0);
1092 match result {
1093 Err(e) => {
1094 let msg = e.to_string();
1095 assert!(
1096 msg.contains("expand_scenario"),
1097 "error must mention expand_scenario, got: {msg}"
1098 );
1099 }
1100 Ok(_) => panic!("csv_replay with columns set must return an error"),
1101 }
1102 }
1103
1104 #[cfg(feature = "config")]
1108 #[test]
1109 fn deserialize_constant_config() {
1110 let yaml = "type: constant\nvalue: 42.0\n";
1111 let config: GeneratorConfig = serde_yaml_ng::from_str(yaml).expect("deserialize constant");
1112 match config {
1113 GeneratorConfig::Constant { value } => {
1114 assert_eq!(value, 42.0);
1115 }
1116 _ => panic!("expected Constant variant"),
1117 }
1118 }
1119
1120 #[cfg(feature = "config")]
1121 #[test]
1122 fn deserialize_uniform_config_with_seed() {
1123 let yaml = "type: uniform\nmin: 1.0\nmax: 5.0\nseed: 99\n";
1124 let config: GeneratorConfig = serde_yaml_ng::from_str(yaml).expect("deserialize uniform");
1125 match config {
1126 GeneratorConfig::Uniform { min, max, seed } => {
1127 assert_eq!(min, 1.0);
1128 assert_eq!(max, 5.0);
1129 assert_eq!(seed, Some(99));
1130 }
1131 _ => panic!("expected Uniform variant"),
1132 }
1133 }
1134
1135 #[cfg(feature = "config")]
1136 #[test]
1137 fn deserialize_uniform_config_without_seed() {
1138 let yaml = "type: uniform\nmin: 0.0\nmax: 10.0\n";
1139 let config: GeneratorConfig =
1140 serde_yaml_ng::from_str(yaml).expect("deserialize uniform no seed");
1141 match config {
1142 GeneratorConfig::Uniform { min, max, seed } => {
1143 assert_eq!(min, 0.0);
1144 assert_eq!(max, 10.0);
1145 assert_eq!(seed, None);
1146 }
1147 _ => panic!("expected Uniform variant"),
1148 }
1149 }
1150
1151 #[cfg(feature = "config")]
1152 #[test]
1153 fn deserialize_sine_config() {
1154 let yaml = "type: sine\namplitude: 5.0\nperiod_secs: 30\noffset: 10.0\n";
1155 let config: GeneratorConfig = serde_yaml_ng::from_str(yaml).expect("deserialize sine");
1156 match config {
1157 GeneratorConfig::Sine {
1158 amplitude,
1159 period_secs,
1160 offset,
1161 } => {
1162 assert_eq!(amplitude, 5.0);
1163 assert_eq!(period_secs, 30.0);
1164 assert_eq!(offset, 10.0);
1165 }
1166 _ => panic!("expected Sine variant"),
1167 }
1168 }
1169
1170 #[cfg(feature = "config")]
1171 #[test]
1172 fn deserialize_sawtooth_config() {
1173 let yaml = "type: sawtooth\nmin: 0.0\nmax: 100.0\nperiod_secs: 60.0\n";
1174 let config: GeneratorConfig = serde_yaml_ng::from_str(yaml).expect("deserialize sawtooth");
1175 match config {
1176 GeneratorConfig::Sawtooth {
1177 min,
1178 max,
1179 period_secs,
1180 } => {
1181 assert_eq!(min, 0.0);
1182 assert_eq!(max, 100.0);
1183 assert_eq!(period_secs, 60.0);
1184 }
1185 _ => panic!("expected Sawtooth variant"),
1186 }
1187 }
1188
1189 #[cfg(feature = "config")]
1190 #[test]
1191 fn deserialize_step_config_full() {
1192 let yaml = "type: step\nstart: 10.0\nstep_size: 2.5\nmax: 100.0\n";
1193 let config: GeneratorConfig = serde_yaml_ng::from_str(yaml).expect("deserialize step");
1194 match config {
1195 GeneratorConfig::Step {
1196 start,
1197 step_size,
1198 max,
1199 } => {
1200 assert_eq!(start, Some(10.0));
1201 assert_eq!(step_size, 2.5);
1202 assert_eq!(max, Some(100.0));
1203 }
1204 _ => panic!("expected Step variant"),
1205 }
1206 }
1207
1208 #[cfg(feature = "config")]
1209 #[test]
1210 fn deserialize_step_config_minimal() {
1211 let yaml = "type: step\nstep_size: 1.0\n";
1212 let config: GeneratorConfig =
1213 serde_yaml_ng::from_str(yaml).expect("deserialize step minimal");
1214 match config {
1215 GeneratorConfig::Step {
1216 start,
1217 step_size,
1218 max,
1219 } => {
1220 assert_eq!(start, None, "start should default to None when omitted");
1221 assert_eq!(step_size, 1.0);
1222 assert_eq!(max, None, "max should be None when omitted");
1223 }
1224 _ => panic!("expected Step variant"),
1225 }
1226 }
1227
1228 #[cfg(feature = "config")]
1229 #[test]
1230 fn deserialize_step_config_integer_values() {
1231 let yaml = "type: step\nstart: 0\nstep_size: 1\nmax: 1000\n";
1233 let config: GeneratorConfig =
1234 serde_yaml_ng::from_str(yaml).expect("deserialize step with integers");
1235 match config {
1236 GeneratorConfig::Step {
1237 start,
1238 step_size,
1239 max,
1240 } => {
1241 assert_eq!(start, Some(0.0));
1242 assert_eq!(step_size, 1.0);
1243 assert_eq!(max, Some(1000.0));
1244 }
1245 _ => panic!("expected Step variant"),
1246 }
1247 }
1248
1249 #[cfg(feature = "config")]
1250 #[test]
1251 fn deserialize_sequence_config_with_repeat() {
1252 let yaml = "type: sequence\nvalues: [1.0, 2.0, 3.0]\nrepeat: true\n";
1253 let config: GeneratorConfig =
1254 serde_yaml_ng::from_str(yaml).expect("deserialize sequence with repeat");
1255 match config {
1256 GeneratorConfig::Sequence { values, repeat } => {
1257 assert_eq!(values, vec![1.0, 2.0, 3.0]);
1258 assert_eq!(repeat, Some(true));
1259 }
1260 _ => panic!("expected Sequence variant"),
1261 }
1262 }
1263
1264 #[cfg(feature = "config")]
1265 #[test]
1266 fn deserialize_sequence_config_without_repeat() {
1267 let yaml = "type: sequence\nvalues: [10.0, 20.0]\n";
1268 let config: GeneratorConfig =
1269 serde_yaml_ng::from_str(yaml).expect("deserialize sequence without repeat");
1270 match config {
1271 GeneratorConfig::Sequence { values, repeat } => {
1272 assert_eq!(values, vec![10.0, 20.0]);
1273 assert_eq!(repeat, None, "repeat should be None when omitted");
1274 }
1275 _ => panic!("expected Sequence variant"),
1276 }
1277 }
1278
1279 #[cfg(feature = "config")]
1280 #[test]
1281 fn deserialize_sequence_config_repeat_false() {
1282 let yaml = "type: sequence\nvalues: [5.0]\nrepeat: false\n";
1283 let config: GeneratorConfig =
1284 serde_yaml_ng::from_str(yaml).expect("deserialize sequence repeat=false");
1285 match config {
1286 GeneratorConfig::Sequence { values, repeat } => {
1287 assert_eq!(values, vec![5.0]);
1288 assert_eq!(repeat, Some(false));
1289 }
1290 _ => panic!("expected Sequence variant"),
1291 }
1292 }
1293
1294 #[cfg(feature = "config")]
1295 #[test]
1296 fn deserialize_sequence_config_integer_values() {
1297 let yaml = "type: sequence\nvalues: [10, 20, 30]\nrepeat: true\n";
1299 let config: GeneratorConfig =
1300 serde_yaml_ng::from_str(yaml).expect("deserialize sequence with integer values");
1301 match config {
1302 GeneratorConfig::Sequence { values, repeat } => {
1303 assert_eq!(values, vec![10.0, 20.0, 30.0]);
1304 assert_eq!(repeat, Some(true));
1305 }
1306 _ => panic!("expected Sequence variant"),
1307 }
1308 }
1309
1310 #[cfg(feature = "config")]
1311 #[test]
1312 fn deserialize_spike_config() {
1313 let yaml =
1314 "type: spike\nbaseline: 50.0\nmagnitude: 200.0\nduration_secs: 10\ninterval_secs: 60\n";
1315 let config: GeneratorConfig = serde_yaml_ng::from_str(yaml).expect("deserialize spike");
1316 match config {
1317 GeneratorConfig::Spike {
1318 baseline,
1319 magnitude,
1320 duration_secs,
1321 interval_secs,
1322 } => {
1323 assert_eq!(baseline, 50.0);
1324 assert_eq!(magnitude, 200.0);
1325 assert_eq!(duration_secs, 10.0);
1326 assert_eq!(interval_secs, 60.0);
1327 }
1328 _ => panic!("expected Spike variant"),
1329 }
1330 }
1331
1332 #[cfg(feature = "config")]
1333 #[test]
1334 fn deserialize_spike_config_negative_magnitude() {
1335 let yaml =
1336 "type: spike\nbaseline: 100.0\nmagnitude: -50.0\nduration_secs: 5\ninterval_secs: 20\n";
1337 let config: GeneratorConfig =
1338 serde_yaml_ng::from_str(yaml).expect("deserialize spike negative magnitude");
1339 match config {
1340 GeneratorConfig::Spike {
1341 baseline,
1342 magnitude,
1343 ..
1344 } => {
1345 assert_eq!(baseline, 100.0);
1346 assert_eq!(magnitude, -50.0);
1347 }
1348 _ => panic!("expected Spike variant"),
1349 }
1350 }
1351
1352 #[cfg(feature = "config")]
1353 #[test]
1354 fn deserialize_example_yaml_scenario_file() {
1355 let yaml = "\
1357name: cpu_spike_test
1358rate: 1
1359duration: 80s
1360
1361generator:
1362 type: sequence
1363 values: [10, 10, 10, 10, 10, 95, 95, 95, 95, 95, 10, 10, 10, 10, 10, 10]
1364 repeat: true
1365
1366labels:
1367 instance: server-01
1368 job: node
1369
1370encoder:
1371 type: prometheus_text
1372sink:
1373 type: stdout
1374";
1375 let config: crate::config::ScenarioConfig =
1376 serde_yaml_ng::from_str(yaml).expect("example YAML must deserialize");
1377 assert_eq!(config.name, "cpu_spike_test");
1378 assert_eq!(config.rate, 1.0);
1379 assert_eq!(config.duration, Some("80s".to_string()));
1380 match &config.generator {
1381 GeneratorConfig::Sequence { values, repeat } => {
1382 assert_eq!(values.len(), 16);
1383 assert_eq!(values[0], 10.0);
1384 assert_eq!(values[5], 95.0);
1385 assert_eq!(values[10], 10.0);
1386 assert_eq!(*repeat, Some(true));
1387 }
1388 _ => panic!("expected Sequence generator variant in example YAML"),
1389 }
1390 }
1391
1392 #[test]
1397 fn wrap_with_jitter_none_returns_unchanged() {
1398 let config = GeneratorConfig::Constant { value: 42.0 };
1399 let gen = create_generator(&config, 1.0).expect("constant factory");
1400 let wrapped = wrap_with_jitter(gen, None, None);
1401 for tick in 0..100 {
1402 assert_eq!(
1403 wrapped.value(tick),
1404 42.0,
1405 "jitter=None must return original values at tick {tick}"
1406 );
1407 }
1408 }
1409
1410 #[test]
1411 fn wrap_with_jitter_zero_returns_unchanged() {
1412 let config = GeneratorConfig::Constant { value: 42.0 };
1413 let gen = create_generator(&config, 1.0).expect("constant factory");
1414 let wrapped = wrap_with_jitter(gen, Some(0.0), Some(99));
1415 for tick in 0..100 {
1416 assert_eq!(
1417 wrapped.value(tick),
1418 42.0,
1419 "jitter=0.0 must return original values at tick {tick}"
1420 );
1421 }
1422 }
1423
1424 #[test]
1425 fn wrap_with_jitter_positive_produces_values_in_range() {
1426 let base = 100.0;
1427 let jitter_amp = 5.0;
1428 let config = GeneratorConfig::Constant { value: base };
1429 let gen = create_generator(&config, 1.0).expect("constant factory");
1430 let wrapped = wrap_with_jitter(gen, Some(jitter_amp), Some(42));
1431 for tick in 0..10_000 {
1432 let v = wrapped.value(tick);
1433 assert!(
1434 v >= base - jitter_amp && v <= base + jitter_amp,
1435 "value {v} at tick {tick} outside [{}, {}]",
1436 base - jitter_amp,
1437 base + jitter_amp
1438 );
1439 }
1440 }
1441
1442 #[test]
1443 fn wrap_with_jitter_seed_none_defaults_to_zero() {
1444 let config = GeneratorConfig::Constant { value: 50.0 };
1445 let gen_none = create_generator(&config, 1.0).expect("factory");
1446 let gen_zero = create_generator(&config, 1.0).expect("factory");
1447 let wrapped_none = wrap_with_jitter(gen_none, Some(5.0), None);
1448 let wrapped_zero = wrap_with_jitter(gen_zero, Some(5.0), Some(0));
1449 for tick in 0..100 {
1450 assert_eq!(
1451 wrapped_none.value(tick),
1452 wrapped_zero.value(tick),
1453 "jitter_seed=None must equal jitter_seed=Some(0) at tick {tick}"
1454 );
1455 }
1456 }
1457
1458 fn assert_send_sync<T: Send + Sync>() {}
1461
1462 #[test]
1463 fn generators_are_send_and_sync() {
1464 assert_send_sync::<crate::generator::uniform::UniformRandom>();
1467 assert_send_sync::<crate::generator::sine::Sine>();
1468 assert_send_sync::<crate::generator::sawtooth::Sawtooth>();
1469 assert_send_sync::<crate::generator::constant::Constant>();
1470 assert_send_sync::<crate::generator::sequence::SequenceGenerator>();
1471 assert_send_sync::<crate::generator::spike::SpikeGenerator>();
1472 assert_send_sync::<crate::generator::csv_replay::CsvReplayGenerator>();
1473 assert_send_sync::<crate::generator::step::StepGenerator>();
1474 assert_send_sync::<crate::generator::jitter::JitterWrapper>();
1475 }
1476
1477 #[cfg(feature = "config")]
1481 #[test]
1482 fn deserialize_log_template_config_minimal() {
1483 let yaml = "\
1484type: template
1485templates:
1486 - message: \"hello {name}\"
1487 field_pools:
1488 name:
1489 - alice
1490 - bob
1491";
1492 let config: LogGeneratorConfig =
1493 serde_yaml_ng::from_str(yaml).expect("deserialize template config");
1494 match config {
1495 LogGeneratorConfig::Template {
1496 templates,
1497 severity_weights,
1498 seed,
1499 } => {
1500 assert_eq!(templates.len(), 1);
1501 assert_eq!(templates[0].message, "hello {name}");
1502 assert!(templates[0].field_pools.contains_key("name"));
1503 assert_eq!(
1504 templates[0].field_pools["name"],
1505 vec!["alice".to_string(), "bob".to_string()]
1506 );
1507 assert!(
1508 severity_weights.is_none(),
1509 "severity_weights must default to None"
1510 );
1511 assert!(seed.is_none(), "seed must default to None");
1512 }
1513 _ => panic!("expected Template variant"),
1514 }
1515 }
1516
1517 #[cfg(feature = "config")]
1518 #[test]
1519 fn deserialize_log_template_config_with_weights_and_seed() {
1520 let yaml = "\
1521type: template
1522templates:
1523 - message: \"msg\"
1524 field_pools: {}
1525severity_weights:
1526 info: 0.7
1527 warn: 0.2
1528 error: 0.1
1529seed: 42
1530";
1531 let config: LogGeneratorConfig =
1532 serde_yaml_ng::from_str(yaml).expect("deserialize template config with weights");
1533 match config {
1534 LogGeneratorConfig::Template {
1535 severity_weights,
1536 seed,
1537 ..
1538 } => {
1539 let weights = severity_weights.expect("severity_weights should be present");
1540 assert!((weights["info"] - 0.7).abs() < 1e-10);
1541 assert!((weights["warn"] - 0.2).abs() < 1e-10);
1542 assert!((weights["error"] - 0.1).abs() < 1e-10);
1543 assert_eq!(seed, Some(42));
1544 }
1545 _ => panic!("expected Template variant"),
1546 }
1547 }
1548
1549 #[cfg(feature = "config")]
1550 #[test]
1551 fn deserialize_log_replay_config() {
1552 let yaml = "type: replay\nfile: /var/log/app.log\n";
1553 let config: LogGeneratorConfig =
1554 serde_yaml_ng::from_str(yaml).expect("deserialize replay config");
1555 match config {
1556 LogGeneratorConfig::Replay { file } => {
1557 assert_eq!(file, "/var/log/app.log");
1558 }
1559 _ => panic!("expected Replay variant"),
1560 }
1561 }
1562
1563 #[test]
1566 fn factory_template_config_creates_working_generator() {
1567 let config = LogGeneratorConfig::Template {
1568 templates: vec![TemplateConfig {
1569 message: "event {id}".into(),
1570 field_pools: {
1571 let mut m = BTreeMap::new();
1572 m.insert("id".into(), vec!["1".into(), "2".into(), "3".into()]);
1573 m
1574 },
1575 }],
1576 severity_weights: None,
1577 seed: Some(0),
1578 };
1579 let gen = create_log_generator(&config).expect("template factory must succeed");
1580 let event = gen.generate(0);
1581 assert!(!event.message.contains('{'));
1583 }
1584
1585 #[test]
1586 fn factory_template_config_seed_none_defaults_correctly() {
1587 let config = LogGeneratorConfig::Template {
1589 templates: vec![TemplateConfig {
1590 message: "static message".into(),
1591 field_pools: BTreeMap::new(),
1592 }],
1593 severity_weights: None,
1594 seed: None,
1595 };
1596 let gen = create_log_generator(&config).expect("template with seed=None must succeed");
1597 assert_eq!(gen.generate(0).message, "static message");
1598 }
1599
1600 #[test]
1601 fn factory_template_invalid_severity_key_returns_error() {
1602 let config = LogGeneratorConfig::Template {
1603 templates: vec![TemplateConfig {
1604 message: "msg".into(),
1605 field_pools: BTreeMap::new(),
1606 }],
1607 severity_weights: {
1608 let mut m = HashMap::new();
1609 m.insert("bogus".into(), 1.0);
1610 Some(m)
1611 },
1612 seed: None,
1613 };
1614 let result = create_log_generator(&config);
1615 assert!(
1616 result.is_err(),
1617 "invalid severity key 'bogus' must produce Err"
1618 );
1619 }
1620
1621 #[test]
1622 fn factory_replay_config_missing_file_returns_error() {
1623 let config = LogGeneratorConfig::Replay {
1624 file: "/this/path/does/not/exist.log".into(),
1625 };
1626 let result = create_log_generator(&config);
1627 assert!(result.is_err(), "missing replay file must produce Err");
1628 }
1629
1630 #[test]
1631 fn factory_replay_config_creates_working_generator() {
1632 use std::io::Write;
1633 use tempfile::NamedTempFile;
1634 let mut tmp = NamedTempFile::new().expect("create temp file");
1635 writeln!(tmp, "line one").expect("write");
1636 writeln!(tmp, "line two").expect("write");
1637 let config = LogGeneratorConfig::Replay {
1638 file: tmp.path().to_string_lossy().into_owned(),
1639 };
1640 let gen =
1641 create_log_generator(&config).expect("replay factory with real file must succeed");
1642 assert_eq!(gen.generate(0).message, "line one");
1643 assert_eq!(gen.generate(1).message, "line two");
1644 assert_eq!(gen.generate(2).message, "line one");
1645 }
1646
1647 #[test]
1648 fn log_generators_are_send_and_sync() {
1649 assert_send_sync::<crate::generator::log_template::LogTemplateGenerator>();
1650 assert_send_sync::<crate::generator::log_replay::LogReplayGenerator>();
1651 }
1652}