1use std::{sync::LazyLock, time::Duration};
8
9use switchy::random::{rng, simulator::seed};
10
11use crate::{RUNS, formatting::TimeFormat as _};
12
13#[derive(Debug, Clone, Copy)]
18pub struct SimConfig {
19 pub seed: u64,
21 pub fail_rate: f64,
23 pub repair_rate: f64,
25 pub tcp_capacity: u64,
27 pub udp_capacity: u64,
29 pub enable_random_order: bool,
31 pub min_message_latency: Duration,
33 pub max_message_latency: Duration,
35 pub duration: Duration,
37 pub tick_duration: Duration,
39 #[cfg(feature = "time")]
41 pub epoch_offset: u64,
42 #[cfg(feature = "time")]
44 pub step_multiplier: u64,
45}
46
47impl Default for SimConfig {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl SimConfig {
54 #[must_use]
58 pub const fn new() -> Self {
59 Self {
60 seed: 0,
61 fail_rate: 0.0,
62 repair_rate: 1.0,
63 tcp_capacity: 64,
64 udp_capacity: 64,
65 enable_random_order: false,
66 min_message_latency: Duration::from_millis(0),
67 max_message_latency: Duration::from_secs(1),
68 duration: Duration::MAX,
69 tick_duration: Duration::from_millis(1),
70 #[cfg(feature = "time")]
71 epoch_offset: 0,
72 #[cfg(feature = "time")]
73 step_multiplier: 1,
74 }
75 }
76
77 #[must_use]
97 pub fn from_rng() -> Self {
98 static DURATION: LazyLock<Duration> = LazyLock::new(|| {
99 std::env::var("SIMULATOR_DURATION")
100 .ok()
101 .map_or(Duration::MAX, |x| {
102 #[allow(clippy::option_if_let_else)]
103 if let Some(x) = x.strip_suffix("µs") {
104 Duration::from_micros(x.parse::<u64>().unwrap())
105 } else if let Some(x) = x.strip_suffix("ns") {
106 Duration::from_nanos(x.parse::<u64>().unwrap())
107 } else if let Some(x) = x.strip_suffix("ms") {
108 Duration::from_millis(x.parse::<u64>().unwrap())
109 } else if let Some(x) = x.strip_suffix("s") {
110 Duration::from_secs(x.parse::<u64>().unwrap())
111 } else {
112 Duration::from_millis(x.parse::<u64>().unwrap())
113 }
114 })
115 });
116
117 let mut config = Self::new();
118 config.seed = seed();
119
120 let min_message_latency = rng().gen_range_dist(0..=1000, 1.0);
121
122 let config = config
123 .fail_rate(0.0)
124 .repair_rate(1.0)
125 .tcp_capacity(64)
126 .udp_capacity(64)
127 .enable_random_order(true)
128 .min_message_latency(Duration::from_millis(min_message_latency))
129 .max_message_latency(Duration::from_millis(
130 rng().gen_range(min_message_latency..2000),
131 ))
132 .duration(*DURATION);
133
134 #[cfg(feature = "time")]
135 {
136 config.epoch_offset = switchy::time::simulator::epoch_offset();
137 config.step_multiplier = switchy::time::simulator::step_multiplier();
138 }
139
140 #[cfg(feature = "time")]
141 let config = config.tick_duration(Duration::from_millis(
142 switchy::time::simulator::step_multiplier(),
143 ));
144
145 *config
146 }
147
148 #[must_use]
150 pub const fn fail_rate(&mut self, fail_rate: f64) -> &mut Self {
151 self.fail_rate = fail_rate;
152 self
153 }
154
155 #[must_use]
157 pub const fn repair_rate(&mut self, repair_rate: f64) -> &mut Self {
158 self.repair_rate = repair_rate;
159 self
160 }
161
162 #[must_use]
164 pub const fn tcp_capacity(&mut self, tcp_capacity: u64) -> &mut Self {
165 self.tcp_capacity = tcp_capacity;
166 self
167 }
168
169 #[must_use]
171 pub const fn udp_capacity(&mut self, udp_capacity: u64) -> &mut Self {
172 self.udp_capacity = udp_capacity;
173 self
174 }
175
176 #[must_use]
178 pub const fn enable_random_order(&mut self, enable_random_order: bool) -> &mut Self {
179 self.enable_random_order = enable_random_order;
180 self
181 }
182
183 #[must_use]
185 pub const fn min_message_latency(&mut self, min_message_latency: Duration) -> &mut Self {
186 self.min_message_latency = min_message_latency;
187 self
188 }
189
190 #[must_use]
192 pub const fn max_message_latency(&mut self, max_message_latency: Duration) -> &mut Self {
193 self.max_message_latency = max_message_latency;
194 self
195 }
196
197 #[must_use]
199 pub const fn duration(&mut self, duration: Duration) -> &mut Self {
200 self.duration = duration;
201 self
202 }
203
204 #[must_use]
206 pub const fn tick_duration(&mut self, tick_duration: Duration) -> &mut Self {
207 self.tick_duration = tick_duration;
208 self
209 }
210}
211
212#[derive(Debug, Clone)]
216pub struct SimProperties {
217 pub config: SimConfig,
219 pub run_number: u64,
221 pub thread_id: Option<u64>,
223 pub extra: Vec<(String, String)>,
225}
226
227#[derive(Debug, Clone)]
231pub struct SimRunProperties {
232 pub steps: u64,
234 pub real_time_millis: u128,
236 pub sim_time_millis: u128,
238}
239
240#[derive(Debug)]
245pub enum SimResult {
246 Success {
248 props: SimProperties,
250 run: SimRunProperties,
252 },
253 Fail {
255 props: SimProperties,
257 run: SimRunProperties,
259 error: Option<String>,
261 panic: Option<String>,
263 },
264}
265
266impl SimResult {
267 #[must_use]
269 pub const fn props(&self) -> &SimProperties {
270 match self {
271 Self::Success { props, .. } | Self::Fail { props, .. } => props,
272 }
273 }
274
275 #[must_use]
277 pub const fn config(&self) -> &SimConfig {
278 &self.props().config
279 }
280
281 #[must_use]
283 pub const fn run(&self) -> &SimRunProperties {
284 match self {
285 Self::Success { run, .. } | Self::Fail { run, .. } => run,
286 }
287 }
288
289 #[must_use]
291 pub const fn is_success(&self) -> bool {
292 matches!(self, Self::Success { .. })
293 }
294}
295
296impl std::fmt::Display for SimResult {
297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298 let props = self.props();
299 let config = &props.config;
300 let run = self.run();
301
302 let run_from_seed = if *RUNS == 1 && switchy::random::simulator::contains_fixed_seed() {
303 String::new()
304 } else {
305 #[cfg(feature = "time")]
306 let failed_epoch_offset = Some(config.epoch_offset);
307 #[cfg(not(feature = "time"))]
308 let failed_epoch_offset = None;
309
310 let cmd = get_run_command(
311 &[
312 "SIMULATOR_SEED",
313 "SIMULATOR_RUNS",
314 "SIMULATOR_DURATION",
315 "SIMULATOR_MAX_PARALLEL",
316 ],
317 config.seed,
318 failed_epoch_offset,
319 );
320 format!("\n\nTo run again with this seed: `{cmd}`")
321 };
322 let run_from_start = if !switchy::random::simulator::contains_fixed_seed() && *RUNS > 1 {
323 let cmd = get_run_command(
324 &["SIMULATOR_SEED"],
325 switchy::random::simulator::initial_seed(),
326 None,
327 );
328 format!("\nTo run entire simulation again from the first run: `{cmd}`")
329 } else {
330 String::new()
331 };
332
333 let (error, panic) = match self {
334 Self::Success { .. } => (String::new(), String::new()),
335 Self::Fail { error, panic, .. } => (
336 error
337 .as_ref()
338 .map_or_else(String::new, |x| format!("\n\nError:\n{x}")),
339 panic
340 .as_ref()
341 .map_or_else(String::new, |x| format!("\n\nPanic:\n{x}")),
342 ),
343 };
344
345 #[allow(clippy::cast_precision_loss)]
346 f.write_fmt(format_args!(
347 "\
348 =========================== FINISH ===========================\n\
349 Server simulator finished\n\n\
350 {run_info}\n\
351 steps={steps}\n\
352 real_time_elapsed={real_time}\n\
353 simulated_time_elapsed={simulated_time} ({simulated_time_x:.2}x)\n\n\
354 successful={successful}\
355 {error}{panic}{run_from_seed}{run_from_start}\n\
356 ==============================================================\
357 ",
358 successful = self.is_success(),
359 run_info = run_info(props),
360 steps = run.steps,
361 real_time = run.real_time_millis.into_formatted(),
362 simulated_time = run.sim_time_millis.into_formatted(),
363 simulated_time_x = run.sim_time_millis as f64 / run.real_time_millis as f64,
364 ))
365 }
366}
367
368#[must_use]
372pub fn run_info(props: &SimProperties) -> String {
373 use std::fmt::Write as _;
374
375 let config = &props.config;
376
377 let mut extra_top = String::new();
378 if let Some(thread_id) = props.thread_id {
379 write!(extra_top, "\nthread_id={thread_id}").unwrap();
380 }
381 #[cfg(feature = "time")]
382 write!(extra_top, "\nepoch_offset={}", config.epoch_offset).unwrap();
383 #[cfg(feature = "time")]
384 write!(extra_top, "\nstep_multiplier={}", config.step_multiplier).unwrap();
385
386 let mut extra_str = String::new();
387 for (k, v) in &props.extra {
388 write!(extra_str, "\n{k}={v}").unwrap();
389 }
390
391 let duration = if config.duration == Duration::MAX {
392 "forever".to_string()
393 } else {
394 config.duration.as_millis().to_string()
395 };
396
397 let run_number = props.run_number;
398 let runs = *RUNS;
399 let runs = if runs > 1 {
400 format!("{run_number}/{runs}")
401 } else {
402 runs.to_string()
403 };
404
405 format!(
406 "\
407 seed={seed}\n\
408 run={runs}{extra_top}\n\
409 tick_duration={tick_duration}\n\
410 fail_rate={fail_rate}\n\
411 repair_rate={repair_rate}\n\
412 tcp_capacity={tcp_capacity}\n\
413 udp_capacity={udp_capacity}\n\
414 enable_random_order={enable_random_order}\n\
415 min_message_latency={min_message_latency}\n\
416 max_message_latency={max_message_latency}\n\
417 duration={duration}{extra_str}\
418 ",
419 seed = config.seed,
420 tick_duration = config.tick_duration.as_millis(),
421 fail_rate = config.fail_rate,
422 repair_rate = config.repair_rate,
423 tcp_capacity = config.tcp_capacity,
424 udp_capacity = config.udp_capacity,
425 enable_random_order = config.enable_random_order,
426 min_message_latency = config.min_message_latency.as_millis(),
427 max_message_latency = config.max_message_latency.as_millis(),
428 )
429}
430
431fn get_cargoified_args() -> Vec<String> {
432 let mut args = std::env::args().collect::<Vec<_>>();
433
434 let Some(cmd) = args.first() else {
435 return args;
436 };
437
438 let mut components = cmd.split('/');
439
440 if matches!(components.next(), Some("target")) {
441 let Some(profile) = components.next() else {
442 return args;
443 };
444 let profile = profile.to_string();
445
446 let Some(binary_name) = components.next() else {
447 return args;
448 };
449 let binary_name = binary_name.to_string();
450
451 args.remove(0);
452 args.insert(0, binary_name);
453 args.insert(0, "-p".to_string());
454
455 if profile == "release" {
456 args.insert(0, "--release".to_string());
457 } else if profile != "debug" {
458 args.insert(0, profile);
459 args.insert(0, "--profile".to_string());
460 }
461
462 args.insert(0, "run".to_string());
463 args.insert(0, "cargo".to_string());
464 }
465
466 args
467}
468
469fn get_run_command(skip_env: &[&str], seed: u64, epoch_offset: Option<u64>) -> String {
470 let args = get_cargoified_args();
471 let quoted_args = args
472 .iter()
473 .map(|x| shell_words::quote(x.as_str()))
474 .collect::<Vec<_>>();
475 let cmd = quoted_args.join(" ");
476
477 let mut env_vars = String::new();
478
479 for (name, value) in std::env::vars() {
480 use std::fmt::Write as _;
481
482 if !name.starts_with("SIMULATOR_") && name != "RUST_LOG" {
483 continue;
484 }
485 if skip_env.iter().any(|x| *x == name) {
486 continue;
487 }
488
489 write!(env_vars, "{name}={} ", shell_words::quote(value.as_str())).unwrap();
490 }
491
492 let mut prefix = format!("SIMULATOR_SEED={seed} ");
493 if let Some(epoch_offset) = epoch_offset {
494 use std::fmt::Write as _;
495 write!(prefix, "SIMULATOR_EPOCH_OFFSET={epoch_offset} ").unwrap();
496 }
497
498 format!("{prefix}{env_vars}{cmd}")
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[cfg(feature = "time")]
506 struct EnvGuard {
507 name: &'static str,
508 value: Option<String>,
509 }
510
511 #[cfg(feature = "time")]
512 impl EnvGuard {
513 fn new(name: &'static str) -> Self {
514 Self {
515 name,
516 value: std::env::var(name).ok(),
517 }
518 }
519
520 fn remove(&self) {
521 unsafe {
522 std::env::remove_var(self.name);
523 }
524 }
525 }
526
527 #[cfg(feature = "time")]
528 impl Drop for EnvGuard {
529 fn drop(&mut self) {
530 match &self.value {
531 Some(value) => unsafe {
532 std::env::set_var(self.name, value);
533 },
534 None => unsafe {
535 std::env::remove_var(self.name);
536 },
537 }
538 }
539 }
540
541 #[test_log::test]
542 #[allow(clippy::float_cmp)]
543 fn test_simconfig_default() {
544 let config = SimConfig::default();
545 assert_eq!(config.seed, 0);
546 assert_eq!(config.fail_rate, 0.0);
547 assert_eq!(config.repair_rate, 1.0);
548 assert_eq!(config.tcp_capacity, 64);
549 assert_eq!(config.udp_capacity, 64);
550 assert!(!config.enable_random_order);
551 assert_eq!(config.min_message_latency, Duration::from_millis(0));
552 assert_eq!(config.max_message_latency, Duration::from_secs(1));
553 assert_eq!(config.duration, Duration::MAX);
554 assert_eq!(config.tick_duration, Duration::from_millis(1));
555 }
556
557 #[test_log::test]
558 #[allow(clippy::float_cmp)]
559 fn test_simconfig_new() {
560 let config = SimConfig::new();
561 assert_eq!(config.seed, 0);
562 assert_eq!(config.fail_rate, 0.0);
563 assert_eq!(config.repair_rate, 1.0);
564 }
565
566 #[test_log::test]
567 #[allow(clippy::float_cmp)]
568 fn test_simconfig_builder_methods() {
569 let mut config = SimConfig::new();
570
571 let _ = config
572 .fail_rate(0.5)
573 .repair_rate(0.8)
574 .tcp_capacity(128)
575 .udp_capacity(256)
576 .enable_random_order(true)
577 .min_message_latency(Duration::from_millis(10))
578 .max_message_latency(Duration::from_secs(2))
579 .duration(Duration::from_mins(1))
580 .tick_duration(Duration::from_millis(5));
581
582 assert_eq!(config.fail_rate, 0.5);
583 assert_eq!(config.repair_rate, 0.8);
584 assert_eq!(config.tcp_capacity, 128);
585 assert_eq!(config.udp_capacity, 256);
586 assert!(config.enable_random_order);
587 assert_eq!(config.min_message_latency, Duration::from_millis(10));
588 assert_eq!(config.max_message_latency, Duration::from_secs(2));
589 assert_eq!(config.duration, Duration::from_mins(1));
590 assert_eq!(config.tick_duration, Duration::from_millis(5));
591 }
592
593 #[test_log::test]
594 #[allow(clippy::float_cmp)]
595 fn test_simconfig_builder_method_chaining() {
596 let mut config = SimConfig::new();
597 let _ = config
598 .fail_rate(0.3)
599 .tcp_capacity(100)
600 .enable_random_order(true);
601
602 assert_eq!(config.fail_rate, 0.3);
603 assert_eq!(config.tcp_capacity, 100);
604 assert!(config.enable_random_order);
605 }
606
607 #[test_log::test]
608 fn test_simresult_is_success() {
609 let props = SimProperties {
610 config: SimConfig::new(),
611 run_number: 1,
612 thread_id: None,
613 extra: vec![],
614 };
615
616 let run = SimRunProperties {
617 steps: 100,
618 real_time_millis: 1000,
619 sim_time_millis: 5000,
620 };
621
622 let success = SimResult::Success {
623 props: props.clone(),
624 run: run.clone(),
625 };
626 assert!(success.is_success());
627
628 let fail = SimResult::Fail {
629 props,
630 run,
631 error: Some("test error".to_string()),
632 panic: None,
633 };
634 assert!(!fail.is_success());
635 }
636
637 #[test_log::test]
638 fn test_simresult_props() {
639 let props = SimProperties {
640 config: SimConfig::new(),
641 run_number: 42,
642 thread_id: Some(3),
643 extra: vec![("key".to_string(), "value".to_string())],
644 };
645
646 let run = SimRunProperties {
647 steps: 100,
648 real_time_millis: 1000,
649 sim_time_millis: 5000,
650 };
651
652 let result = SimResult::Success { props, run };
653
654 let result_props = result.props();
655 assert_eq!(result_props.run_number, 42);
656 assert_eq!(result_props.thread_id, Some(3));
657 assert_eq!(result_props.extra.len(), 1);
658 }
659
660 #[test_log::test]
661 fn test_simresult_config() {
662 let mut config = SimConfig::new();
663 let _ = config.tcp_capacity(256);
664
665 let props = SimProperties {
666 config,
667 run_number: 1,
668 thread_id: None,
669 extra: vec![],
670 };
671
672 let run = SimRunProperties {
673 steps: 100,
674 real_time_millis: 1000,
675 sim_time_millis: 5000,
676 };
677
678 let result = SimResult::Success { props, run };
679
680 assert_eq!(result.config().tcp_capacity, 256);
681 }
682
683 #[test_log::test]
684 fn test_simresult_run() {
685 let props = SimProperties {
686 config: SimConfig::new(),
687 run_number: 1,
688 thread_id: None,
689 extra: vec![],
690 };
691
692 let run = SimRunProperties {
693 steps: 12345,
694 real_time_millis: 9876,
695 sim_time_millis: 54321,
696 };
697
698 let result = SimResult::Success { props, run };
699
700 let result_run = result.run();
701 assert_eq!(result_run.steps, 12345);
702 assert_eq!(result_run.real_time_millis, 9876);
703 assert_eq!(result_run.sim_time_millis, 54321);
704 }
705
706 #[test_log::test]
707 fn test_get_cargoified_args_with_target_path() {
708 let args = get_cargoified_args();
711 assert!(!args.is_empty());
712 }
713
714 #[test_log::test]
715 fn test_simresult_props_for_fail_variant() {
716 let props = SimProperties {
717 config: SimConfig::new(),
718 run_number: 10,
719 thread_id: Some(5),
720 extra: vec![("debug".to_string(), "true".to_string())],
721 };
722
723 let run = SimRunProperties {
724 steps: 500,
725 real_time_millis: 2000,
726 sim_time_millis: 10000,
727 };
728
729 let fail = SimResult::Fail {
730 props,
731 run,
732 error: Some("test error".to_string()),
733 panic: Some("test panic".to_string()),
734 };
735
736 let result_props = fail.props();
737 assert_eq!(result_props.run_number, 10);
738 assert_eq!(result_props.thread_id, Some(5));
739 assert_eq!(result_props.extra.len(), 1);
740 }
741
742 #[test_log::test]
743 fn test_simresult_run_for_fail_variant() {
744 let props = SimProperties {
745 config: SimConfig::new(),
746 run_number: 1,
747 thread_id: None,
748 extra: vec![],
749 };
750
751 let run = SimRunProperties {
752 steps: 999,
753 real_time_millis: 5555,
754 sim_time_millis: 8888,
755 };
756
757 let fail = SimResult::Fail {
758 props,
759 run,
760 error: Some("error".to_string()),
761 panic: None,
762 };
763
764 let result_run = fail.run();
765 assert_eq!(result_run.steps, 999);
766 assert_eq!(result_run.real_time_millis, 5555);
767 assert_eq!(result_run.sim_time_millis, 8888);
768 }
769
770 #[test_log::test]
771 fn test_simresult_config_for_fail_variant() {
772 let mut config = SimConfig::new();
773 let _ = config.udp_capacity(512);
774
775 let props = SimProperties {
776 config,
777 run_number: 1,
778 thread_id: None,
779 extra: vec![],
780 };
781
782 let run = SimRunProperties {
783 steps: 100,
784 real_time_millis: 1000,
785 sim_time_millis: 5000,
786 };
787
788 let fail = SimResult::Fail {
789 props,
790 run,
791 error: None,
792 panic: Some("panic message".to_string()),
793 };
794
795 assert_eq!(fail.config().udp_capacity, 512);
796 }
797
798 #[test_log::test]
799 fn test_run_info_contains_config_values() {
800 let mut config = SimConfig::new();
801 let _ = config
802 .fail_rate(0.5)
803 .tcp_capacity(128)
804 .enable_random_order(true);
805
806 let props = SimProperties {
807 config,
808 run_number: 1,
809 thread_id: None,
810 extra: vec![],
811 };
812
813 let info = run_info(&props);
814
815 assert!(info.contains("seed=0"));
816 assert!(info.contains("fail_rate=0.5"));
817 assert!(info.contains("tcp_capacity=128"));
818 assert!(info.contains("enable_random_order=true"));
819 }
820
821 #[test_log::test]
822 fn test_run_info_includes_thread_id_when_present() {
823 let props = SimProperties {
824 config: SimConfig::new(),
825 run_number: 1,
826 thread_id: Some(42),
827 extra: vec![],
828 };
829
830 let info = run_info(&props);
831
832 assert!(info.contains("thread_id=42"));
833 }
834
835 #[test_log::test]
836 fn test_run_info_excludes_thread_id_when_none() {
837 let props = SimProperties {
838 config: SimConfig::new(),
839 run_number: 1,
840 thread_id: None,
841 extra: vec![],
842 };
843
844 let info = run_info(&props);
845
846 assert!(!info.contains("thread_id="));
847 }
848
849 #[test_log::test]
850 fn test_run_info_includes_extra_properties() {
851 let props = SimProperties {
852 config: SimConfig::new(),
853 run_number: 1,
854 thread_id: None,
855 extra: vec![
856 ("custom_key".to_string(), "custom_value".to_string()),
857 ("another_key".to_string(), "another_value".to_string()),
858 ],
859 };
860
861 let info = run_info(&props);
862
863 assert!(info.contains("custom_key=custom_value"));
864 assert!(info.contains("another_key=another_value"));
865 }
866
867 #[test_log::test]
868 fn test_run_info_duration_forever_when_max() {
869 let props = SimProperties {
870 config: SimConfig::new(), run_number: 1,
872 thread_id: None,
873 extra: vec![],
874 };
875
876 let info = run_info(&props);
877
878 assert!(info.contains("duration=forever"));
879 }
880
881 #[test_log::test]
882 fn test_run_info_duration_value_when_finite() {
883 let mut config = SimConfig::new();
884 let _ = config.duration(Duration::from_mins(2));
885
886 let props = SimProperties {
887 config,
888 run_number: 1,
889 thread_id: None,
890 extra: vec![],
891 };
892
893 let info = run_info(&props);
894
895 assert!(info.contains("duration=120000"));
897 }
898
899 #[cfg(feature = "time")]
900 #[test_log::test]
901 fn test_fail_output_includes_seed_and_epoch_offset_rerun_command() {
902 let seed_guard = EnvGuard::new("SIMULATOR_SEED");
903 let epoch_guard = EnvGuard::new("SIMULATOR_EPOCH_OFFSET");
904 seed_guard.remove();
905 epoch_guard.remove();
906
907 let mut config = SimConfig::new();
908 config.seed = 4242;
909 config.epoch_offset = 1_700_000_000_000;
910
911 let props = SimProperties {
912 config,
913 run_number: 1,
914 thread_id: None,
915 extra: vec![],
916 };
917
918 let run = SimRunProperties {
919 steps: 10,
920 real_time_millis: 100,
921 sim_time_millis: 200,
922 };
923
924 let result = SimResult::Fail {
925 props,
926 run,
927 error: Some("failure".to_string()),
928 panic: None,
929 };
930
931 let output = result.to_string();
932 assert!(output.contains("To run again with this seed:"));
933 assert!(output.contains("SIMULATOR_SEED=4242"));
934 assert!(output.contains("SIMULATOR_EPOCH_OFFSET=1700000000000"));
935 }
936}