1use std::borrow::Cow;
15use std::collections::{BTreeMap, HashSet};
16use std::ffi::{CStr, CString, OsStr, OsString};
17use std::os::unix::ffi::OsStrExt;
18use std::str::FromStr;
19
20use clap::Parser;
21use logger as c_log;
22use merge::Merge;
23use once_cell::sync::Lazy;
24use schemars::{JsonSchema, schema_for};
25use serde::{Deserialize, Serialize};
26use shadow_shim_helper_rs::simulation_time::SimulationTime;
27
28use crate::cshadow as c;
29use crate::host::syscall::formatter::FmtOptions;
30use crate::utility::units::{self, Unit};
31
32const START_HELP_TEXT: &str = "\
33 Run real applications over simulated networks.\n\n\
34 For documentation, visit https://shadow.github.io/docs/guide";
35
36const END_HELP_TEXT: &str = "\
37 If units are not specified, all values are assumed to be given in their base \
38 unit (seconds, bytes, bits, etc). Units can optionally be specified (for \
39 example: '1024 B', '1024 bytes', '1 KiB', '1 kibibyte', etc) and are \
40 case-sensitive.";
41
42static VERSION: Lazy<String> = Lazy::new(crate::shadow::version);
44
45#[derive(Debug, Clone, Parser)]
46#[clap(name = "Shadow", about = START_HELP_TEXT, after_help = END_HELP_TEXT)]
47#[clap(version = VERSION.as_str())]
48#[clap(next_display_order = None)]
49#[clap(hide_possible_values = true)]
52pub struct CliOptions {
53 #[clap(required_unless_present_any(&["show_build_info", "shm_cleanup"]))]
55 pub config: Option<String>,
56
57 #[clap(long, short = 'g')]
59 pub gdb: bool,
60
61 #[clap(value_parser = parse_set_str)]
63 #[clap(long, value_name = "hostnames")]
64 pub debug_hosts: Option<HashSet<String>>,
65
66 #[clap(long, exclusive(true))]
68 pub shm_cleanup: bool,
69
70 #[clap(long, exclusive(true))]
72 pub show_build_info: bool,
73
74 #[clap(long)]
76 pub show_config: bool,
77
78 #[clap(flatten)]
79 pub general: GeneralOptions,
80
81 #[clap(flatten)]
82 pub network: NetworkOptions,
83
84 #[clap(flatten)]
85 pub host_option_defaults: HostDefaultOptions,
86
87 #[clap(flatten)]
88 pub experimental: ExperimentalOptions,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93#[serde(deny_unknown_fields)]
94pub struct ConfigFileOptions {
95 pub general: GeneralOptions,
96
97 pub network: NetworkOptions,
98
99 #[serde(default)]
100 pub host_option_defaults: HostDefaultOptions,
101
102 #[serde(default)]
103 pub experimental: ExperimentalOptions,
104
105 pub hosts: BTreeMap<HostName, HostOptions>,
109}
110
111#[derive(Debug, Clone, Serialize)]
113pub struct ConfigOptions {
114 pub general: GeneralOptions,
115
116 pub network: NetworkOptions,
117
118 pub experimental: ExperimentalOptions,
119
120 pub hosts: BTreeMap<HostName, HostOptions>,
122}
123
124impl ConfigOptions {
125 pub fn new(mut config_file: ConfigFileOptions, options: CliOptions) -> Self {
126 config_file.host_option_defaults = config_file
129 .host_option_defaults
130 .with_defaults(HostDefaultOptions::new_with_defaults());
131
132 config_file.general = options.general.with_defaults(config_file.general);
134 config_file.network = options.network.with_defaults(config_file.network);
135 config_file.host_option_defaults = options
136 .host_option_defaults
137 .with_defaults(config_file.host_option_defaults);
138 config_file.experimental = options.experimental.with_defaults(config_file.experimental);
139
140 for host in config_file.hosts.values_mut() {
142 host.host_options = host
143 .host_options
144 .clone()
145 .with_defaults(config_file.host_option_defaults.clone());
146 }
147
148 Self {
149 general: config_file.general,
150 network: config_file.network,
151 experimental: config_file.experimental,
152 hosts: config_file.hosts,
153 }
154 }
155
156 pub fn model_unblocked_syscall_latency(&self) -> bool {
157 self.general.model_unblocked_syscall_latency.unwrap()
158 }
159
160 pub fn max_unapplied_cpu_latency(&self) -> SimulationTime {
161 let nanos = self.experimental.max_unapplied_cpu_latency.unwrap();
162 let nanos = nanos.convert(units::TimePrefix::Nano).unwrap().value();
163 SimulationTime::from_nanos(nanos)
164 }
165
166 pub fn unblocked_syscall_latency(&self) -> SimulationTime {
167 let nanos = self.experimental.unblocked_syscall_latency.unwrap();
168 let nanos = nanos.convert(units::TimePrefix::Nano).unwrap().value();
169 SimulationTime::from_nanos(nanos)
170 }
171
172 pub fn unblocked_vdso_latency(&self) -> SimulationTime {
173 let nanos = self.experimental.unblocked_vdso_latency.unwrap();
174 let nanos = nanos.convert(units::TimePrefix::Nano).unwrap().value();
175 SimulationTime::from_nanos(nanos)
176 }
177
178 pub fn native_preemption_enabled(&self) -> bool {
179 self.experimental.native_preemption_enabled.unwrap()
180 }
181
182 pub fn native_preemption_native_interval(
183 &self,
184 ) -> anyhow::Result<linux_api::time::kernel_old_timeval> {
185 let t = self.experimental.native_preemption_native_interval.unwrap();
186 let t = core::time::Duration::from(t);
187 if t < core::time::Duration::from_micros(1) {
191 return Err(anyhow::anyhow!(
192 "native_preemption_native_interval must be >= 1 microsecond. Got {t:?}."
193 ));
194 }
195 let rv = linux_api::time::kernel_old_timeval {
196 tv_sec: t.as_secs().try_into().unwrap(),
197 tv_usec: t.subsec_micros().into(),
198 };
199 assert!(!(rv.tv_sec == 0 && rv.tv_usec == 0));
200 Ok(rv)
201 }
202
203 pub fn native_preemption_sim_interval(&self) -> SimulationTime {
204 let t = self.experimental.native_preemption_sim_interval.unwrap();
205 let nanos = t.convert(units::TimePrefix::Nano).unwrap().value();
206 SimulationTime::from_nanos(nanos)
207 }
208
209 pub fn strace_logging_mode(&self) -> Option<FmtOptions> {
210 match self.experimental.strace_logging_mode.as_ref().unwrap() {
211 StraceLoggingMode::Standard => Some(FmtOptions::Standard),
212 StraceLoggingMode::Deterministic => Some(FmtOptions::Deterministic),
213 StraceLoggingMode::Long => Some(FmtOptions::Long),
214 StraceLoggingMode::Off => None,
215 }
216 }
217}
218
219static GENERAL_HELP: Lazy<std::collections::HashMap<String, String>> =
222 Lazy::new(|| generate_help_strs(schema_for!(GeneralOptions)));
223
224#[derive(Debug, Clone, Parser, Serialize, Deserialize, Merge, JsonSchema)]
227#[clap(next_help_heading = "General (Override configuration file options)")]
228#[clap(next_display_order = None)]
229#[serde(deny_unknown_fields)]
230#[merge(strategy = merge::option::overwrite_none)]
231pub struct GeneralOptions {
232 #[clap(long, value_name = "seconds")]
234 #[clap(help = GENERAL_HELP.get("stop_time").unwrap().as_str())]
235 pub stop_time: Option<units::Time<units::TimePrefix>>,
236
237 #[clap(long, value_name = "N")]
239 #[clap(help = GENERAL_HELP.get("seed").unwrap().as_str())]
240 #[serde(default = "default_some_1")]
241 pub seed: Option<u32>,
242
243 #[clap(long, short = 'p', value_name = "cores")]
246 #[clap(help = GENERAL_HELP.get("parallelism").unwrap().as_str())]
247 #[serde(default = "default_some_0")]
248 pub parallelism: Option<u32>,
249
250 #[clap(long, value_name = "seconds")]
252 #[clap(help = GENERAL_HELP.get("bootstrap_end_time").unwrap().as_str())]
253 #[serde(default = "default_some_time_0")]
254 pub bootstrap_end_time: Option<units::Time<units::TimePrefix>>,
255
256 #[clap(long, short = 'l', value_name = "level")]
259 #[clap(help = GENERAL_HELP.get("log_level").unwrap().as_str())]
260 #[serde(default = "default_some_info")]
261 pub log_level: Option<LogLevel>,
262
263 #[clap(long, value_name = "seconds")]
265 #[clap(help = GENERAL_HELP.get("heartbeat_interval").unwrap().as_str())]
266 #[serde(default = "default_some_nullable_time_1")]
267 pub heartbeat_interval: Option<NullableOption<units::Time<units::TimePrefix>>>,
268
269 #[clap(long, short = 'd', value_name = "path")]
271 #[clap(help = GENERAL_HELP.get("data_directory").unwrap().as_str())]
272 #[serde(default = "default_data_directory")]
273 pub data_directory: Option<String>,
274
275 #[clap(long, short = 'e', value_name = "path")]
277 #[clap(help = GENERAL_HELP.get("template_directory").unwrap().as_str())]
278 #[serde(default)]
279 pub template_directory: Option<NullableOption<String>>,
280
281 #[clap(long, value_name = "bool")]
283 #[clap(help = GENERAL_HELP.get("progress").unwrap().as_str())]
284 #[serde(default = "default_some_false")]
285 pub progress: Option<bool>,
286
287 #[clap(long, value_name = "bool")]
292 #[clap(help = GENERAL_HELP.get("model_unblocked_syscall_latency").unwrap().as_str())]
293 #[serde(default = "default_some_false")]
294 pub model_unblocked_syscall_latency: Option<bool>,
295}
296
297impl GeneralOptions {
298 pub fn with_defaults(mut self, default: Self) -> Self {
300 self.merge(default);
301 self
302 }
303}
304
305static NETWORK_HELP: Lazy<std::collections::HashMap<String, String>> =
308 Lazy::new(|| generate_help_strs(schema_for!(NetworkOptions)));
309
310#[derive(Debug, Clone, Parser, Serialize, Deserialize, Merge, JsonSchema)]
313#[clap(next_help_heading = "Network (Override network options)")]
314#[clap(next_display_order = None)]
315#[serde(deny_unknown_fields)]
316#[merge(strategy = merge::option::overwrite_none)]
317pub struct NetworkOptions {
318 #[clap(skip)]
320 pub graph: Option<GraphOptions>,
321
322 #[serde(default = "default_some_true")]
325 #[clap(long, value_name = "bool")]
326 #[clap(help = NETWORK_HELP.get("use_shortest_path").unwrap().as_str())]
327 pub use_shortest_path: Option<bool>,
328}
329
330impl NetworkOptions {
331 pub fn with_defaults(mut self, default: Self) -> Self {
333 self.merge(default);
334 self
335 }
336}
337
338static EXP_HELP: Lazy<std::collections::HashMap<String, String>> =
341 Lazy::new(|| generate_help_strs(schema_for!(ExperimentalOptions)));
342
343#[derive(Debug, Clone, Parser, Serialize, Deserialize, Merge, JsonSchema)]
344#[clap(
345 next_help_heading = "Experimental (Unstable and may change or be removed at any time, regardless of Shadow version)"
346)]
347#[clap(next_display_order = None)]
348#[serde(default, deny_unknown_fields)]
349#[merge(strategy = merge::option::overwrite_none)]
350pub struct ExperimentalOptions {
351 #[clap(hide_short_help = true)]
353 #[clap(long, value_name = "bool")]
354 #[clap(help = EXP_HELP.get("use_sched_fifo").unwrap().as_str())]
355 pub use_sched_fifo: Option<bool>,
356
357 #[clap(hide_short_help = true)]
359 #[clap(long, value_name = "bool")]
360 #[clap(help = EXP_HELP.get("use_syscall_counters").unwrap().as_str())]
361 pub use_syscall_counters: Option<bool>,
362
363 #[clap(hide_short_help = true)]
365 #[clap(long, value_name = "bool")]
366 #[clap(help = EXP_HELP.get("use_object_counters").unwrap().as_str())]
367 pub use_object_counters: Option<bool>,
368
369 #[clap(hide_short_help = true)]
371 #[clap(long, value_name = "bool")]
372 #[clap(help = EXP_HELP.get("use_preload_libc").unwrap().as_str())]
373 pub use_preload_libc: Option<bool>,
374
375 #[clap(hide_short_help = true)]
377 #[clap(long, value_name = "bool")]
378 #[clap(help = EXP_HELP.get("use_preload_openssl_rng").unwrap().as_str())]
379 pub use_preload_openssl_rng: Option<bool>,
380
381 #[clap(hide_short_help = true)]
385 #[clap(long, value_name = "bool")]
386 #[clap(help = EXP_HELP.get("use_preload_openssl_crypto").unwrap().as_str())]
387 pub use_preload_openssl_crypto: Option<bool>,
388
389 #[clap(hide_short_help = true)]
393 #[clap(long, value_name = "bool")]
394 #[clap(help = EXP_HELP.get("use_memory_manager").unwrap().as_str())]
395 pub use_memory_manager: Option<bool>,
396
397 #[clap(hide_short_help = true)]
399 #[clap(long, value_name = "bool")]
400 #[clap(help = EXP_HELP.get("use_cpu_pinning").unwrap().as_str())]
401 pub use_cpu_pinning: Option<bool>,
402
403 #[clap(hide_short_help = true)]
406 #[clap(long, value_name = "bool")]
407 #[clap(help = EXP_HELP.get("use_worker_spinning").unwrap().as_str())]
408 pub use_worker_spinning: Option<bool>,
409
410 #[clap(hide_short_help = true)]
412 #[clap(long, value_name = "seconds")]
413 #[clap(help = EXP_HELP.get("runahead").unwrap().as_str())]
414 pub runahead: Option<NullableOption<units::Time<units::TimePrefix>>>,
415
416 #[clap(hide_short_help = true)]
418 #[clap(long, value_name = "bool")]
419 #[clap(help = EXP_HELP.get("use_dynamic_runahead").unwrap().as_str())]
420 pub use_dynamic_runahead: Option<bool>,
421
422 #[clap(hide_short_help = true)]
424 #[clap(long, value_name = "bytes")]
425 #[clap(help = EXP_HELP.get("socket_send_buffer").unwrap().as_str())]
426 pub socket_send_buffer: Option<units::Bytes<units::SiPrefixUpper>>,
427
428 #[clap(hide_short_help = true)]
430 #[clap(long, value_name = "bool")]
431 #[clap(help = EXP_HELP.get("socket_send_autotune").unwrap().as_str())]
432 pub socket_send_autotune: Option<bool>,
433
434 #[clap(hide_short_help = true)]
436 #[clap(long, value_name = "bytes")]
437 #[clap(help = EXP_HELP.get("socket_recv_buffer").unwrap().as_str())]
438 pub socket_recv_buffer: Option<units::Bytes<units::SiPrefixUpper>>,
439
440 #[clap(hide_short_help = true)]
442 #[clap(long, value_name = "bool")]
443 #[clap(help = EXP_HELP.get("socket_recv_autotune").unwrap().as_str())]
444 pub socket_recv_autotune: Option<bool>,
445
446 #[clap(hide_short_help = true)]
448 #[clap(long, value_name = "mode")]
449 #[clap(help = EXP_HELP.get("interface_qdisc").unwrap().as_str())]
450 pub interface_qdisc: Option<QDiscMode>,
451
452 #[clap(hide_short_help = true)]
454 #[clap(long, value_name = "mode")]
455 #[clap(help = EXP_HELP.get("strace_logging_mode").unwrap().as_str())]
456 pub strace_logging_mode: Option<StraceLoggingMode>,
457
458 #[clap(hide_short_help = true)]
464 #[clap(long, value_name = "seconds")]
465 #[clap(help = EXP_HELP.get("max_unapplied_cpu_latency").unwrap().as_str())]
466 pub max_unapplied_cpu_latency: Option<units::Time<units::TimePrefix>>,
467
468 #[clap(hide_short_help = true)]
472 #[clap(long, value_name = "seconds")]
473 #[clap(help = EXP_HELP.get("unblocked_syscall_latency").unwrap().as_str())]
474 pub unblocked_syscall_latency: Option<units::Time<units::TimePrefix>>,
475
476 #[clap(hide_short_help = true)]
480 #[clap(long, value_name = "seconds")]
481 #[clap(help = EXP_HELP.get("unblocked_vdso_latency").unwrap().as_str())]
482 pub unblocked_vdso_latency: Option<units::Time<units::TimePrefix>>,
483
484 #[clap(hide_short_help = true)]
487 #[clap(long, value_name = "name")]
488 #[clap(help = EXP_HELP.get("scheduler").unwrap().as_str())]
489 pub scheduler: Option<Scheduler>,
490
491 #[clap(hide_short_help = true)]
493 #[clap(long, value_name = "bool")]
494 #[clap(help = EXP_HELP.get("report_errors_to_stderr").unwrap().as_str())]
495 pub report_errors_to_stderr: Option<bool>,
496
497 #[clap(hide_short_help = true)]
499 #[clap(long, value_name = "bool")]
500 #[clap(help = EXP_HELP.get("use_new_tcp").unwrap().as_str())]
501 pub use_new_tcp: Option<bool>,
502
503 #[clap(hide_short_help = true)]
510 #[clap(long, value_name = "bool")]
511 #[clap(help = EXP_HELP.get("native_preemption_enabled").unwrap().as_str())]
512 pub native_preemption_enabled: Option<bool>,
513
514 #[clap(hide_short_help = true)]
519 #[clap(long, value_name = "seconds")]
520 #[clap(help = EXP_HELP.get("native_preemption_native_interval").unwrap().as_str())]
521 pub native_preemption_native_interval: Option<units::Time<units::TimePrefix>>,
522
523 #[clap(hide_short_help = true)]
527 #[clap(long, value_name = "seconds")]
528 #[clap(help = EXP_HELP.get("native_preemption_sim_interval").unwrap().as_str())]
529 pub native_preemption_sim_interval: Option<units::Time<units::TimePrefix>>,
530}
531
532impl ExperimentalOptions {
533 pub fn with_defaults(mut self, default: Self) -> Self {
535 self.merge(default);
536 self
537 }
538}
539
540impl Default for ExperimentalOptions {
541 fn default() -> Self {
542 Self {
543 use_sched_fifo: Some(false),
544 use_syscall_counters: Some(true),
545 use_object_counters: Some(true),
546 use_preload_libc: Some(true),
547 use_preload_openssl_rng: Some(true),
548 use_preload_openssl_crypto: Some(false),
549 max_unapplied_cpu_latency: Some(units::Time::new(1, units::TimePrefix::Micro)),
550 unblocked_syscall_latency: Some(units::Time::new(1, units::TimePrefix::Micro)),
554 unblocked_vdso_latency: Some(units::Time::new(10, units::TimePrefix::Nano)),
557 use_memory_manager: Some(false),
558 use_cpu_pinning: Some(true),
559 use_worker_spinning: Some(true),
560 runahead: Some(NullableOption::Value(units::Time::new(
561 1,
562 units::TimePrefix::Milli,
563 ))),
564 use_dynamic_runahead: Some(false),
565 socket_send_buffer: Some(units::Bytes::new(131_072, units::SiPrefixUpper::Base)),
566 socket_send_autotune: Some(true),
567 socket_recv_buffer: Some(units::Bytes::new(174_760, units::SiPrefixUpper::Base)),
568 socket_recv_autotune: Some(true),
569 interface_qdisc: Some(QDiscMode::Fifo),
570 strace_logging_mode: Some(StraceLoggingMode::Off),
571 scheduler: Some(Scheduler::ThreadPerCore),
572 report_errors_to_stderr: Some(true),
573 use_new_tcp: Some(false),
574 native_preemption_enabled: Some(false),
575 native_preemption_native_interval: Some(units::Time::new(
576 100,
577 units::TimePrefix::Milli,
578 )),
579 native_preemption_sim_interval: Some(units::Time::new(10, units::TimePrefix::Milli)),
580 }
581 }
582}
583
584static HOST_HELP: Lazy<std::collections::HashMap<String, String>> =
587 Lazy::new(|| generate_help_strs(schema_for!(HostDefaultOptions)));
588
589#[derive(Debug, Clone, Parser, Serialize, Deserialize, Merge, JsonSchema)]
590#[clap(next_help_heading = "Host Defaults (Default options for hosts)")]
591#[clap(next_display_order = None)]
592#[serde(default, deny_unknown_fields)]
593#[schemars(default = "HostDefaultOptions::new_with_defaults")]
595#[merge(strategy = merge::option::overwrite_none)]
596pub struct HostDefaultOptions {
597 #[clap(long = "host-log-level", name = "host-log-level")]
599 #[clap(value_name = "level")]
600 #[clap(help = HOST_HELP.get("log_level").unwrap().as_str())]
601 pub log_level: Option<NullableOption<LogLevel>>,
602
603 #[clap(long, value_name = "bool")]
605 #[clap(help = HOST_HELP.get("pcap_enabled").unwrap().as_str())]
606 pub pcap_enabled: Option<bool>,
607
608 #[clap(long, value_name = "bytes")]
610 #[clap(help = HOST_HELP.get("pcap_capture_size").unwrap().as_str())]
611 pub pcap_capture_size: Option<units::Bytes<units::SiPrefixUpper>>,
612}
613
614impl HostDefaultOptions {
615 pub fn new_with_defaults() -> Self {
616 Self {
617 log_level: None,
618 pcap_enabled: Some(false),
619 pcap_capture_size: Some(units::Bytes::new(65535, units::SiPrefixUpper::Base)),
623 }
624 }
625
626 pub fn with_defaults(mut self, default: Self) -> Self {
628 self.merge(default);
629 self
630 }
631}
632
633#[allow(clippy::derivable_impls)]
634impl Default for HostDefaultOptions {
635 fn default() -> Self {
636 Self {
642 log_level: None,
643 pcap_enabled: None,
644 pcap_capture_size: None,
645 }
646 }
647}
648
649#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Copy, Clone, JsonSchema)]
650#[serde(rename_all = "kebab-case")]
651pub enum RunningVal {
652 Running,
653}
654
655#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
658#[serde(untagged)]
659pub enum ProcessFinalState {
660 Exited { exited: i32 },
661 Signaled { signaled: Signal },
662 Running(RunningVal),
663}
664
665impl Default for ProcessFinalState {
666 fn default() -> Self {
667 Self::Exited { exited: 0 }
668 }
669}
670
671impl std::fmt::Display for ProcessFinalState {
672 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
673 let s = serde_yaml::to_string(self).or(Err(std::fmt::Error))?;
683 write!(f, "{}", s.trim())
684 }
685}
686
687#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
688#[serde(deny_unknown_fields)]
689pub struct ProcessOptions {
690 pub path: std::path::PathBuf,
691
692 #[serde(default = "default_args_empty")]
694 pub args: ProcessArgs,
695
696 #[serde(default)]
698 pub environment: BTreeMap<EnvName, String>,
699
700 #[serde(default)]
702 pub start_time: units::Time<units::TimePrefix>,
703
704 #[serde(default)]
706 pub shutdown_time: Option<units::Time<units::TimePrefix>>,
707
708 #[serde(default = "default_sigterm")]
710 pub shutdown_signal: Signal,
711
712 #[serde(default)]
715 pub expected_final_state: ProcessFinalState,
716}
717
718#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
719#[serde(deny_unknown_fields)]
720pub struct HostOptions {
721 pub network_node_id: u32,
723
724 pub processes: Vec<ProcessOptions>,
725
726 #[serde(default)]
728 pub ip_addr: Option<std::net::Ipv4Addr>,
729
730 #[serde(default)]
732 pub bandwidth_down: Option<units::BitsPerSec<units::SiPrefixUpper>>,
733
734 #[serde(default)]
736 pub bandwidth_up: Option<units::BitsPerSec<units::SiPrefixUpper>>,
737
738 #[serde(default)]
739 pub host_options: HostDefaultOptions,
740}
741
742#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
743#[serde(rename_all = "kebab-case")]
744pub enum LogLevel {
745 Error,
746 Warning,
747 Info,
748 Debug,
749 Trace,
750}
751
752impl FromStr for LogLevel {
753 type Err = serde_yaml::Error;
754
755 fn from_str(s: &str) -> Result<Self, Self::Err> {
756 serde_yaml::from_str(s)
757 }
758}
759
760impl LogLevel {
761 pub fn to_c_loglevel(&self) -> c_log::LogLevel {
762 match self {
763 Self::Error => c_log::_LogLevel_LOGLEVEL_ERROR,
764 Self::Warning => c_log::_LogLevel_LOGLEVEL_WARNING,
765 Self::Info => c_log::_LogLevel_LOGLEVEL_INFO,
766 Self::Debug => c_log::_LogLevel_LOGLEVEL_DEBUG,
767 Self::Trace => c_log::_LogLevel_LOGLEVEL_TRACE,
768 }
769 }
770}
771
772impl From<LogLevel> for log::Level {
773 fn from(level: LogLevel) -> Self {
774 match level {
775 LogLevel::Error => log::Level::Error,
776 LogLevel::Warning => log::Level::Warn,
777 LogLevel::Info => log::Level::Info,
778 LogLevel::Debug => log::Level::Debug,
779 LogLevel::Trace => log::Level::Trace,
780 }
781 }
782}
783
784#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Serialize, JsonSchema)]
785pub struct HostName(String);
786
787impl<'de> serde::Deserialize<'de> for HostName {
788 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
789 struct HostNameVisitor;
790
791 impl serde::de::Visitor<'_> for HostNameVisitor {
792 type Value = HostName;
793
794 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
795 formatter.write_str("a string")
796 }
797
798 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
799 where
800 E: serde::de::Error,
801 {
802 fn is_allowed(c: char) -> bool {
805 c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '.'
806 }
807 if let Some(invalid_char) = v.chars().find(|x| !is_allowed(*x)) {
808 return Err(E::custom(format!(
809 "invalid hostname character: '{invalid_char}'"
810 )));
811 }
812
813 if v.is_empty() {
814 return Err(E::custom("empty hostname"));
815 }
816
817 if v.starts_with('-') {
819 return Err(E::custom("hostname begins with a '-' character"));
820 }
821
822 if v.len() > 253 {
825 return Err(E::custom("hostname exceeds 253 characters"));
826 }
827
828 Ok(HostName(v))
829 }
830
831 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
832 where
833 E: serde::de::Error,
834 {
835 self.visit_string(v.to_string())
838 }
839 }
840
841 deserializer.deserialize_string(HostNameVisitor)
842 }
843}
844
845impl std::ops::Deref for HostName {
846 type Target = String;
847
848 fn deref(&self) -> &Self::Target {
849 &self.0
850 }
851}
852
853impl From<HostName> for String {
854 fn from(name: HostName) -> Self {
855 name.0
856 }
857}
858
859impl std::fmt::Display for HostName {
860 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
861 self.0.fmt(f)
862 }
863}
864
865#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Serialize, JsonSchema)]
866pub struct EnvName(String);
867
868impl EnvName {
869 pub fn new(name: impl Into<String>) -> Option<Self> {
870 let name = name.into();
871
872 if name.contains('=') {
874 return None;
875 }
876
877 Some(Self(name))
878 }
879}
880
881impl<'de> serde::Deserialize<'de> for EnvName {
882 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
883 struct EnvNameVisitor;
884
885 impl serde::de::Visitor<'_> for EnvNameVisitor {
886 type Value = EnvName;
887
888 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
889 formatter.write_str("a string")
890 }
891
892 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
893 where
894 E: serde::de::Error,
895 {
896 let Some(name) = EnvName::new(v) else {
897 let e = "environment variable name contains a '=' character";
898 return Err(E::custom(e));
899 };
900
901 Ok(name)
902 }
903
904 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
905 where
906 E: serde::de::Error,
907 {
908 self.visit_string(v.to_string())
911 }
912 }
913
914 deserializer.deserialize_string(EnvNameVisitor)
915 }
916}
917
918impl std::ops::Deref for EnvName {
919 type Target = String;
920
921 fn deref(&self) -> &Self::Target {
922 &self.0
923 }
924}
925
926impl From<EnvName> for String {
927 fn from(name: EnvName) -> Self {
928 name.0
929 }
930}
931
932impl std::fmt::Display for EnvName {
933 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
934 self.0.fmt(f)
935 }
936}
937
938#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
939#[serde(rename_all = "kebab-case")]
940pub enum Scheduler {
941 ThreadPerHost,
942 ThreadPerCore,
943}
944
945impl FromStr for Scheduler {
946 type Err = serde_yaml::Error;
947
948 fn from_str(s: &str) -> Result<Self, Self::Err> {
949 serde_yaml::from_str(s)
950 }
951}
952
953fn default_data_directory() -> Option<String> {
954 Some("shadow.data".into())
955}
956
957fn parse_set<T>(s: &str) -> Result<HashSet<T>, <T as FromStr>::Err>
959where
960 T: std::cmp::Eq + std::hash::Hash + FromStr,
961{
962 s.split(',').map(|x| x.trim().parse()).collect()
963}
964
965fn parse_set_str(s: &str) -> Result<HashSet<String>, <String as FromStr>::Err> {
967 parse_set(s)
968}
969
970#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
971#[serde(rename_all = "kebab-case")]
972#[repr(C)]
973pub enum QDiscMode {
974 Fifo,
975 RoundRobin,
976}
977
978impl FromStr for QDiscMode {
979 type Err = serde_yaml::Error;
980
981 fn from_str(s: &str) -> Result<Self, Self::Err> {
982 serde_yaml::from_str(s)
983 }
984}
985
986#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
987#[serde(rename_all = "kebab-case")]
988pub enum Compression {
989 Xz,
990}
991
992#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
993#[serde(deny_unknown_fields)]
994pub struct FileSource {
995 pub path: String,
997 pub compression: Option<Compression>,
999}
1000
1001#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1002#[serde(rename_all = "kebab-case")]
1003pub enum GraphSource {
1004 File(FileSource),
1005 Inline(String),
1006}
1007
1008#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1009#[serde(tag = "type", rename_all = "snake_case")]
1012pub enum GraphOptions {
1013 Gml(GraphSource),
1014 #[serde(rename = "1_gbit_switch")]
1015 OneGbitSwitch,
1016}
1017
1018#[derive(Debug, Clone, Serialize, JsonSchema)]
1019#[serde(untagged)]
1020pub enum ProcessArgs {
1021 List(Vec<String>),
1022 Str(String),
1023}
1024
1025impl<'de> serde::Deserialize<'de> for ProcessArgs {
1032 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1033 struct ProcessArgsVisitor;
1034
1035 impl<'de> serde::de::Visitor<'de> for ProcessArgsVisitor {
1036 type Value = ProcessArgs;
1037
1038 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1039 formatter.write_str("a string or a sequence of strings")
1040 }
1041
1042 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1043 where
1044 E: serde::de::Error,
1045 {
1046 Ok(Self::Value::Str(v.to_owned()))
1047 }
1048
1049 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
1050 where
1051 A: serde::de::SeqAccess<'de>,
1052 {
1053 let mut v = vec![];
1054
1055 while let Some(val) = seq.next_element()? {
1056 v.push(val);
1057 }
1058
1059 Ok(Self::Value::List(v))
1060 }
1061 }
1062
1063 deserializer.deserialize_any(ProcessArgsVisitor)
1064 }
1065}
1066
1067#[derive(Debug, Copy, Clone, Eq, PartialEq)]
1072pub struct Signal(nix::sys::signal::Signal);
1073
1074impl From<nix::sys::signal::Signal> for Signal {
1075 fn from(value: nix::sys::signal::Signal) -> Self {
1076 Self(value)
1077 }
1078}
1079
1080impl TryFrom<linux_api::signal::Signal> for Signal {
1081 type Error = <nix::sys::signal::Signal as TryFrom<i32>>::Error;
1082 fn try_from(value: linux_api::signal::Signal) -> Result<Self, Self::Error> {
1083 let signal = nix::sys::signal::Signal::try_from(value.as_i32())?;
1084 Ok(Self(signal))
1085 }
1086}
1087
1088impl serde::Serialize for Signal {
1089 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1090 where
1091 S: serde::Serializer,
1092 {
1093 serializer.serialize_str(self.0.as_str())
1094 }
1095}
1096
1097impl<'de> serde::Deserialize<'de> for Signal {
1098 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1099 struct SignalVisitor;
1100
1101 impl serde::de::Visitor<'_> for SignalVisitor {
1102 type Value = Signal;
1103
1104 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1105 formatter.write_str("a signal string (e.g. \"SIGINT\") or integer")
1106 }
1107
1108 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1109 where
1110 E: serde::de::Error,
1111 {
1112 nix::sys::signal::Signal::from_str(v)
1113 .map(Signal)
1114 .map_err(|_e| E::custom(format!("Invalid signal string: {v}")))
1115 }
1116
1117 fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
1118 where
1119 E: serde::de::Error,
1120 {
1121 let v = i32::try_from(v)
1122 .map_err(|_e| E::custom(format!("Invalid signal number: {v}")))?;
1123 nix::sys::signal::Signal::try_from(v)
1124 .map(Signal)
1125 .map_err(|_e| E::custom(format!("Invalid signal number: {v}")))
1126 }
1127
1128 fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
1129 where
1130 E: serde::de::Error,
1131 {
1132 let v = i64::try_from(v)
1133 .map_err(|_e| E::custom(format!("Invalid signal number: {v}")))?;
1134 self.visit_i64(v)
1135 }
1136 }
1137
1138 deserializer.deserialize_any(SignalVisitor)
1139 }
1140}
1141
1142impl std::fmt::Display for Signal {
1143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1144 write!(f, "{}", self.0)
1145 }
1146}
1147
1148impl JsonSchema for Signal {
1149 fn schema_name() -> Cow<'static, str> {
1150 "Signal".into()
1151 }
1152
1153 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1154 schemars::json_schema!(true)
1162 }
1163}
1164
1165impl std::ops::Deref for Signal {
1166 type Target = nix::sys::signal::Signal;
1167
1168 fn deref(&self) -> &Self::Target {
1169 &self.0
1170 }
1171}
1172
1173#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1174#[serde(rename_all = "kebab-case")]
1175pub enum StraceLoggingMode {
1176 Off,
1177 Standard,
1178 Deterministic,
1179 Long,
1180}
1181
1182impl FromStr for StraceLoggingMode {
1183 type Err = serde_yaml::Error;
1184
1185 fn from_str(s: &str) -> Result<Self, Self::Err> {
1186 serde_yaml::from_str(s)
1187 }
1188}
1189
1190#[derive(Debug, Copy, Clone, JsonSchema, Eq, PartialEq)]
1244pub enum NullableOption<T> {
1245 Value(T),
1246 Null,
1247}
1248
1249impl<T> NullableOption<T> {
1250 pub fn as_ref(&self) -> NullableOption<&T> {
1251 match self {
1252 NullableOption::Value(x) => NullableOption::Value(x),
1253 NullableOption::Null => NullableOption::Null,
1254 }
1255 }
1256
1257 pub fn as_mut(&mut self) -> NullableOption<&mut T> {
1258 match self {
1259 NullableOption::Value(x) => NullableOption::Value(x),
1260 NullableOption::Null => NullableOption::Null,
1261 }
1262 }
1263
1264 pub fn to_option(self) -> Option<T> {
1267 match self {
1268 NullableOption::Value(x) => Some(x),
1269 NullableOption::Null => None,
1270 }
1271 }
1272}
1273
1274impl<T: serde::Serialize> serde::Serialize for NullableOption<T> {
1275 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1276 match self {
1277 Self::Value(x) => Ok(T::serialize(x, serializer)?),
1279 Self::Null => serializer.serialize_none(),
1280 }
1281 }
1282}
1283
1284impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for NullableOption<T> {
1285 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1286 Ok(Self::Value(T::deserialize(deserializer)?))
1288 }
1289}
1290
1291impl<T> FromStr for NullableOption<T>
1292where
1293 T: FromStr<Err: std::fmt::Debug + std::fmt::Display>,
1294{
1295 type Err = T::Err;
1296
1297 fn from_str(s: &str) -> Result<Self, Self::Err> {
1298 match s {
1299 "null" => Ok(Self::Null),
1301 x => Ok(Self::Value(FromStr::from_str(x)?)),
1302 }
1303 }
1304}
1305
1306pub trait Flatten<T> {
1308 fn flatten(self) -> Option<T>;
1309 fn flatten_ref(&self) -> Option<&T>;
1310}
1311
1312impl<T> Flatten<T> for Option<NullableOption<T>> {
1313 fn flatten(self) -> Option<T> {
1314 self.and_then(|x| x.to_option())
1315 }
1316
1317 fn flatten_ref(&self) -> Option<&T> {
1318 self.as_ref().and_then(|x| x.as_ref().to_option())
1319 }
1320}
1321
1322fn default_args_empty() -> ProcessArgs {
1324 ProcessArgs::Str("".to_string())
1325}
1326
1327fn default_sigterm() -> Signal {
1329 Signal(nix::sys::signal::Signal::SIGTERM)
1330}
1331
1332fn default_some_time_0() -> Option<units::Time<units::TimePrefix>> {
1334 Some(units::Time::new(0, units::TimePrefix::Sec))
1335}
1336
1337fn default_some_true() -> Option<bool> {
1339 Some(true)
1340}
1341
1342fn default_some_false() -> Option<bool> {
1344 Some(false)
1345}
1346
1347fn default_some_0() -> Option<u32> {
1349 Some(0)
1350}
1351
1352fn default_some_1() -> Option<u32> {
1354 Some(1)
1355}
1356
1357fn default_some_nullable_time_1() -> Option<NullableOption<units::Time<units::TimePrefix>>> {
1359 let time = units::Time::new(1, units::TimePrefix::Sec);
1360 Some(NullableOption::Value(time))
1361}
1362
1363fn default_some_info() -> Option<LogLevel> {
1365 Some(LogLevel::Info)
1366}
1367
1368pub const ONE_GBIT_SWITCH_GRAPH: &str = r#"graph [
1370 directed 0
1371 node [
1372 id 0
1373 host_bandwidth_up "1 Gbit"
1374 host_bandwidth_down "1 Gbit"
1375 ]
1376 edge [
1377 source 0
1378 target 0
1379 latency "1 ms"
1380 packet_loss 0.0
1381 ]
1382]"#;
1383
1384fn generate_help_strs(schema: schemars::Schema) -> std::collections::HashMap<String, String> {
1386 let mut defaults = std::collections::HashMap::<String, String>::new();
1388
1389 for (name, obj) in schema.get("properties").unwrap().as_object().unwrap() {
1391 let description = obj
1392 .get("description")
1393 .map(|x| x.as_str().unwrap())
1394 .unwrap_or("");
1395
1396 let description = description.replace("\n", " ");
1407
1408 let name = name.clone();
1409
1410 match obj.get("default") {
1411 Some(default) => {
1412 let space = if !description.is_empty() { " " } else { "" };
1413 defaults.insert(name, format!("{description}{space}[default: {default}]"))
1414 }
1415 None => defaults.insert(name, description.to_string()),
1416 };
1417 }
1418
1419 defaults
1420}
1421
1422pub fn parse_string_as_args(args_str: &OsStr) -> Result<Vec<OsString>, String> {
1425 if args_str.is_empty() {
1426 return Ok(Vec::new());
1427 }
1428
1429 let args_str = CString::new(args_str.as_bytes()).unwrap();
1430
1431 let mut argc: libc::c_int = 0;
1433 let mut argv: *mut *mut libc::c_char = std::ptr::null_mut();
1434 let mut error: *mut libc::c_char = std::ptr::null_mut();
1435 let rv = unsafe { c::process_parseArgStr(args_str.as_ptr(), &mut argc, &mut argv, &mut error) };
1436
1437 if !rv {
1439 let error_message = match error.is_null() {
1440 false => unsafe { CStr::from_ptr(error) }.to_str().unwrap(),
1441 true => "Unknown parsing error",
1442 }
1443 .to_string();
1444
1445 unsafe { c::process_parseArgStrFree(argv, error) };
1446 return Err(error_message);
1447 }
1448
1449 assert!(!argv.is_null());
1450
1451 let args: Vec<_> = (0..argc)
1453 .map(|x| unsafe {
1454 let arg_ptr = *argv.add(x as usize);
1455 assert!(!arg_ptr.is_null());
1456 OsStr::from_bytes(CStr::from_ptr(arg_ptr).to_bytes()).to_os_string()
1457 })
1458 .collect();
1459
1460 unsafe { c::process_parseArgStrFree(argv, error) };
1461 Ok(args)
1462}
1463
1464#[cfg(test)]
1465mod tests {
1466 use super::*;
1467
1468 #[test]
1469 #[cfg_attr(miri, ignore)]
1471 fn test_parse_args() {
1472 let arg_str = r#"the quick brown fox "jumped over" the "\"lazy\" dog""#;
1473 let expected_args = &[
1474 "the",
1475 "quick",
1476 "brown",
1477 "fox",
1478 "jumped over",
1479 "the",
1480 "\"lazy\" dog",
1481 ];
1482
1483 let arg_str: OsString = arg_str.into();
1484 let args = parse_string_as_args(&arg_str).unwrap();
1485
1486 assert_eq!(args, expected_args);
1487 }
1488
1489 #[test]
1490 #[cfg_attr(miri, ignore)]
1492 fn test_parse_args_empty() {
1493 let arg_str = "";
1494 let expected_args: &[&str] = &[];
1495
1496 let arg_str: OsString = arg_str.into();
1497 let args = parse_string_as_args(&arg_str).unwrap();
1498
1499 assert_eq!(args, expected_args);
1500 }
1501
1502 #[test]
1503 #[cfg_attr(miri, ignore)]
1505 fn test_parse_args_error() {
1506 let arg_str = r#"hello "world"#;
1507
1508 let arg_str: OsString = arg_str.into();
1509 let err_str = parse_string_as_args(&arg_str).unwrap_err();
1510
1511 assert!(!err_str.is_empty());
1512 }
1513
1514 #[test]
1515 #[cfg_attr(miri, ignore)]
1517 fn test_nullable_option() {
1518 let yaml_fmt_fn = |option| {
1520 format!(
1521 r#"
1522 general:
1523 stop_time: 1 min
1524 {option}
1525 network:
1526 graph:
1527 type: 1_gbit_switch
1528 hosts:
1529 myhost:
1530 network_node_id: 0
1531 processes:
1532 - path: /bin/true
1533 "#,
1534 )
1535 };
1536
1537 let time_1_sec = units::Time::new(1, units::TimePrefix::Sec);
1538 let time_5_sec = units::Time::new(5, units::TimePrefix::Sec);
1539
1540 let yaml = yaml_fmt_fn("heartbeat_interval: null");
1542 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1543 let cli: CliOptions = CliOptions::try_parse_from(["shadow", "-"]).unwrap();
1544
1545 let merged = ConfigOptions::new(config_file, cli);
1546 assert_eq!(merged.general.heartbeat_interval, None);
1547
1548 let yaml = yaml_fmt_fn("heartbeat_interval: null");
1550 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1551 let cli: CliOptions =
1552 CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "5s", "-"]).unwrap();
1553
1554 let merged = ConfigOptions::new(config_file, cli);
1555 assert_eq!(
1556 merged.general.heartbeat_interval,
1557 Some(NullableOption::Value(time_5_sec))
1558 );
1559
1560 let yaml = yaml_fmt_fn("heartbeat_interval: null");
1562 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1563 let cli: CliOptions =
1564 CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "null", "-"]).unwrap();
1565
1566 let merged = ConfigOptions::new(config_file, cli);
1567 assert_eq!(
1568 merged.general.heartbeat_interval,
1569 Some(NullableOption::Null)
1570 );
1571
1572 let yaml = yaml_fmt_fn("heartbeat_interval: 5s");
1574 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1575 let cli: CliOptions = CliOptions::try_parse_from(["shadow", "-"]).unwrap();
1576
1577 let merged = ConfigOptions::new(config_file, cli);
1578 assert_eq!(
1579 merged.general.heartbeat_interval,
1580 Some(NullableOption::Value(time_5_sec))
1581 );
1582
1583 let yaml = yaml_fmt_fn("heartbeat_interval: 5s");
1585 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1586 let cli: CliOptions =
1587 CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "5s", "-"]).unwrap();
1588
1589 let merged = ConfigOptions::new(config_file, cli);
1590 assert_eq!(
1591 merged.general.heartbeat_interval,
1592 Some(NullableOption::Value(time_5_sec))
1593 );
1594
1595 let yaml = yaml_fmt_fn("heartbeat_interval: 5s");
1597 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1598 let cli: CliOptions =
1599 CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "null", "-"]).unwrap();
1600
1601 let merged = ConfigOptions::new(config_file, cli);
1602 assert_eq!(
1603 merged.general.heartbeat_interval,
1604 Some(NullableOption::Null)
1605 );
1606
1607 let yaml = yaml_fmt_fn("");
1609 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1610 let cli: CliOptions = CliOptions::try_parse_from(["shadow", "-"]).unwrap();
1611
1612 let merged = ConfigOptions::new(config_file, cli);
1613 assert_eq!(
1614 merged.general.heartbeat_interval,
1615 Some(NullableOption::Value(time_1_sec))
1616 );
1617
1618 let yaml = yaml_fmt_fn("");
1620 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1621 let cli: CliOptions =
1622 CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "5s", "-"]).unwrap();
1623
1624 let merged = ConfigOptions::new(config_file, cli);
1625 assert_eq!(
1626 merged.general.heartbeat_interval,
1627 Some(NullableOption::Value(time_5_sec))
1628 );
1629
1630 let yaml = yaml_fmt_fn("");
1632 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1633 let cli: CliOptions =
1634 CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "null", "-"]).unwrap();
1635
1636 let merged = ConfigOptions::new(config_file, cli);
1637 assert_eq!(
1638 merged.general.heartbeat_interval,
1639 Some(NullableOption::Null)
1640 );
1641 }
1642}