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)]
391 #[clap(long, value_name = "bool")]
392 #[clap(help = EXP_HELP.get("use_cpu_pinning").unwrap().as_str())]
393 pub use_cpu_pinning: Option<bool>,
394
395 #[clap(hide_short_help = true)]
398 #[clap(long, value_name = "bool")]
399 #[clap(help = EXP_HELP.get("use_worker_spinning").unwrap().as_str())]
400 pub use_worker_spinning: Option<bool>,
401
402 #[clap(hide_short_help = true)]
404 #[clap(long, value_name = "seconds")]
405 #[clap(help = EXP_HELP.get("runahead").unwrap().as_str())]
406 pub runahead: Option<NullableOption<units::Time<units::TimePrefix>>>,
407
408 #[clap(hide_short_help = true)]
410 #[clap(long, value_name = "bool")]
411 #[clap(help = EXP_HELP.get("use_dynamic_runahead").unwrap().as_str())]
412 pub use_dynamic_runahead: Option<bool>,
413
414 #[clap(hide_short_help = true)]
416 #[clap(long, value_name = "bytes")]
417 #[clap(help = EXP_HELP.get("socket_send_buffer").unwrap().as_str())]
418 pub socket_send_buffer: Option<units::Bytes<units::SiPrefixUpper>>,
419
420 #[clap(hide_short_help = true)]
422 #[clap(long, value_name = "bool")]
423 #[clap(help = EXP_HELP.get("socket_send_autotune").unwrap().as_str())]
424 pub socket_send_autotune: Option<bool>,
425
426 #[clap(hide_short_help = true)]
428 #[clap(long, value_name = "bytes")]
429 #[clap(help = EXP_HELP.get("socket_recv_buffer").unwrap().as_str())]
430 pub socket_recv_buffer: Option<units::Bytes<units::SiPrefixUpper>>,
431
432 #[clap(hide_short_help = true)]
434 #[clap(long, value_name = "bool")]
435 #[clap(help = EXP_HELP.get("socket_recv_autotune").unwrap().as_str())]
436 pub socket_recv_autotune: Option<bool>,
437
438 #[clap(hide_short_help = true)]
440 #[clap(long, value_name = "mode")]
441 #[clap(help = EXP_HELP.get("interface_qdisc").unwrap().as_str())]
442 pub interface_qdisc: Option<QDiscMode>,
443
444 #[clap(hide_short_help = true)]
446 #[clap(long, value_name = "mode")]
447 #[clap(help = EXP_HELP.get("strace_logging_mode").unwrap().as_str())]
448 pub strace_logging_mode: Option<StraceLoggingMode>,
449
450 #[clap(hide_short_help = true)]
456 #[clap(long, value_name = "seconds")]
457 #[clap(help = EXP_HELP.get("max_unapplied_cpu_latency").unwrap().as_str())]
458 pub max_unapplied_cpu_latency: Option<units::Time<units::TimePrefix>>,
459
460 #[clap(hide_short_help = true)]
464 #[clap(long, value_name = "seconds")]
465 #[clap(help = EXP_HELP.get("unblocked_syscall_latency").unwrap().as_str())]
466 pub unblocked_syscall_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_vdso_latency").unwrap().as_str())]
474 pub unblocked_vdso_latency: Option<units::Time<units::TimePrefix>>,
475
476 #[clap(hide_short_help = true)]
479 #[clap(long, value_name = "name")]
480 #[clap(help = EXP_HELP.get("scheduler").unwrap().as_str())]
481 pub scheduler: Option<Scheduler>,
482
483 #[clap(hide_short_help = true)]
485 #[clap(long, value_name = "bool")]
486 #[clap(help = EXP_HELP.get("report_errors_to_stderr").unwrap().as_str())]
487 pub report_errors_to_stderr: Option<bool>,
488
489 #[clap(hide_short_help = true)]
491 #[clap(long, value_name = "bool")]
492 #[clap(help = EXP_HELP.get("use_new_tcp").unwrap().as_str())]
493 pub use_new_tcp: Option<bool>,
494
495 #[clap(hide_short_help = true)]
502 #[clap(long, value_name = "bool")]
503 #[clap(help = EXP_HELP.get("native_preemption_enabled").unwrap().as_str())]
504 pub native_preemption_enabled: Option<bool>,
505
506 #[clap(hide_short_help = true)]
511 #[clap(long, value_name = "seconds")]
512 #[clap(help = EXP_HELP.get("native_preemption_native_interval").unwrap().as_str())]
513 pub native_preemption_native_interval: Option<units::Time<units::TimePrefix>>,
514
515 #[clap(hide_short_help = true)]
519 #[clap(long, value_name = "seconds")]
520 #[clap(help = EXP_HELP.get("native_preemption_sim_interval").unwrap().as_str())]
521 pub native_preemption_sim_interval: Option<units::Time<units::TimePrefix>>,
522}
523
524impl ExperimentalOptions {
525 pub fn with_defaults(mut self, default: Self) -> Self {
527 self.merge(default);
528 self
529 }
530}
531
532impl Default for ExperimentalOptions {
533 fn default() -> Self {
534 Self {
535 use_sched_fifo: Some(false),
536 use_syscall_counters: Some(true),
537 use_object_counters: Some(true),
538 use_preload_libc: Some(true),
539 use_preload_openssl_rng: Some(true),
540 use_preload_openssl_crypto: Some(false),
541 max_unapplied_cpu_latency: Some(units::Time::new(1, units::TimePrefix::Micro)),
542 unblocked_syscall_latency: Some(units::Time::new(1, units::TimePrefix::Micro)),
546 unblocked_vdso_latency: Some(units::Time::new(10, units::TimePrefix::Nano)),
549 use_cpu_pinning: Some(true),
550 use_worker_spinning: Some(true),
551 runahead: Some(NullableOption::Value(units::Time::new(
552 1,
553 units::TimePrefix::Milli,
554 ))),
555 use_dynamic_runahead: Some(false),
556 socket_send_buffer: Some(units::Bytes::new(131_072, units::SiPrefixUpper::Base)),
557 socket_send_autotune: Some(true),
558 socket_recv_buffer: Some(units::Bytes::new(174_760, units::SiPrefixUpper::Base)),
559 socket_recv_autotune: Some(true),
560 interface_qdisc: Some(QDiscMode::Fifo),
561 strace_logging_mode: Some(StraceLoggingMode::Off),
562 scheduler: Some(Scheduler::ThreadPerCore),
563 report_errors_to_stderr: Some(true),
564 use_new_tcp: Some(false),
565 native_preemption_enabled: Some(false),
566 native_preemption_native_interval: Some(units::Time::new(
567 100,
568 units::TimePrefix::Milli,
569 )),
570 native_preemption_sim_interval: Some(units::Time::new(10, units::TimePrefix::Milli)),
571 }
572 }
573}
574
575static HOST_HELP: Lazy<std::collections::HashMap<String, String>> =
578 Lazy::new(|| generate_help_strs(schema_for!(HostDefaultOptions)));
579
580#[derive(Debug, Clone, Parser, Serialize, Deserialize, Merge, JsonSchema)]
581#[clap(next_help_heading = "Host Defaults (Default options for hosts)")]
582#[clap(next_display_order = None)]
583#[serde(default, deny_unknown_fields)]
584#[schemars(default = "HostDefaultOptions::new_with_defaults")]
586#[merge(strategy = merge::option::overwrite_none)]
587pub struct HostDefaultOptions {
588 #[clap(long = "host-log-level", name = "host-log-level")]
590 #[clap(value_name = "level")]
591 #[clap(help = HOST_HELP.get("log_level").unwrap().as_str())]
592 pub log_level: Option<NullableOption<LogLevel>>,
593
594 #[clap(long, value_name = "bool")]
596 #[clap(help = HOST_HELP.get("pcap_enabled").unwrap().as_str())]
597 pub pcap_enabled: Option<bool>,
598
599 #[clap(long, value_name = "bytes")]
601 #[clap(help = HOST_HELP.get("pcap_capture_size").unwrap().as_str())]
602 pub pcap_capture_size: Option<units::Bytes<units::SiPrefixUpper>>,
603}
604
605impl HostDefaultOptions {
606 pub fn new_with_defaults() -> Self {
607 Self {
608 log_level: None,
609 pcap_enabled: Some(false),
610 pcap_capture_size: Some(units::Bytes::new(65535, units::SiPrefixUpper::Base)),
614 }
615 }
616
617 pub fn with_defaults(mut self, default: Self) -> Self {
619 self.merge(default);
620 self
621 }
622}
623
624#[allow(clippy::derivable_impls)]
625impl Default for HostDefaultOptions {
626 fn default() -> Self {
627 Self {
633 log_level: None,
634 pcap_enabled: None,
635 pcap_capture_size: None,
636 }
637 }
638}
639
640#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Copy, Clone, JsonSchema)]
641#[serde(rename_all = "kebab-case")]
642pub enum RunningVal {
643 Running,
644}
645
646#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
649#[serde(untagged)]
650pub enum ProcessFinalState {
651 Exited { exited: i32 },
652 Signaled { signaled: Signal },
653 Running(RunningVal),
654}
655
656impl Default for ProcessFinalState {
657 fn default() -> Self {
658 Self::Exited { exited: 0 }
659 }
660}
661
662impl std::fmt::Display for ProcessFinalState {
663 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
664 let s = serde_yaml::to_string(self).or(Err(std::fmt::Error))?;
674 write!(f, "{}", s.trim())
675 }
676}
677
678#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
679#[serde(deny_unknown_fields)]
680pub struct ProcessOptions {
681 pub path: std::path::PathBuf,
682
683 #[serde(default = "default_args_empty")]
685 pub args: ProcessArgs,
686
687 #[serde(default)]
689 pub environment: BTreeMap<EnvName, String>,
690
691 #[serde(default)]
693 pub start_time: units::Time<units::TimePrefix>,
694
695 #[serde(default)]
697 pub shutdown_time: Option<units::Time<units::TimePrefix>>,
698
699 #[serde(default = "default_sigterm")]
701 pub shutdown_signal: Signal,
702
703 #[serde(default)]
706 pub expected_final_state: ProcessFinalState,
707}
708
709#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
710#[serde(deny_unknown_fields)]
711pub struct HostOptions {
712 pub network_node_id: u32,
714
715 pub processes: Vec<ProcessOptions>,
716
717 #[serde(default)]
719 pub ip_addr: Option<std::net::Ipv4Addr>,
720
721 #[serde(default)]
723 pub bandwidth_down: Option<units::BitsPerSec<units::SiPrefixUpper>>,
724
725 #[serde(default)]
727 pub bandwidth_up: Option<units::BitsPerSec<units::SiPrefixUpper>>,
728
729 #[serde(default)]
730 pub host_options: HostDefaultOptions,
731}
732
733#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
734#[serde(rename_all = "kebab-case")]
735pub enum LogLevel {
736 Error,
737 Warning,
738 Info,
739 Debug,
740 Trace,
741}
742
743impl FromStr for LogLevel {
744 type Err = serde_yaml::Error;
745
746 fn from_str(s: &str) -> Result<Self, Self::Err> {
747 serde_yaml::from_str(s)
748 }
749}
750
751impl LogLevel {
752 pub fn to_c_loglevel(&self) -> c_log::LogLevel {
753 match self {
754 Self::Error => c_log::_LogLevel_LOGLEVEL_ERROR,
755 Self::Warning => c_log::_LogLevel_LOGLEVEL_WARNING,
756 Self::Info => c_log::_LogLevel_LOGLEVEL_INFO,
757 Self::Debug => c_log::_LogLevel_LOGLEVEL_DEBUG,
758 Self::Trace => c_log::_LogLevel_LOGLEVEL_TRACE,
759 }
760 }
761}
762
763impl From<LogLevel> for log::Level {
764 fn from(level: LogLevel) -> Self {
765 match level {
766 LogLevel::Error => log::Level::Error,
767 LogLevel::Warning => log::Level::Warn,
768 LogLevel::Info => log::Level::Info,
769 LogLevel::Debug => log::Level::Debug,
770 LogLevel::Trace => log::Level::Trace,
771 }
772 }
773}
774
775#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Serialize, JsonSchema)]
776pub struct HostName(String);
777
778impl<'de> serde::Deserialize<'de> for HostName {
779 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
780 struct HostNameVisitor;
781
782 impl serde::de::Visitor<'_> for HostNameVisitor {
783 type Value = HostName;
784
785 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
786 formatter.write_str("a string")
787 }
788
789 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
790 where
791 E: serde::de::Error,
792 {
793 fn is_allowed(c: char) -> bool {
796 c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '.'
797 }
798 if let Some(invalid_char) = v.chars().find(|x| !is_allowed(*x)) {
799 return Err(E::custom(format!(
800 "invalid hostname character: '{invalid_char}'"
801 )));
802 }
803
804 if v.is_empty() {
805 return Err(E::custom("empty hostname"));
806 }
807
808 if v.starts_with('-') {
810 return Err(E::custom("hostname begins with a '-' character"));
811 }
812
813 if v.len() > 253 {
816 return Err(E::custom("hostname exceeds 253 characters"));
817 }
818
819 Ok(HostName(v))
820 }
821
822 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
823 where
824 E: serde::de::Error,
825 {
826 self.visit_string(v.to_string())
829 }
830 }
831
832 deserializer.deserialize_string(HostNameVisitor)
833 }
834}
835
836impl std::ops::Deref for HostName {
837 type Target = String;
838
839 fn deref(&self) -> &Self::Target {
840 &self.0
841 }
842}
843
844impl From<HostName> for String {
845 fn from(name: HostName) -> Self {
846 name.0
847 }
848}
849
850impl std::fmt::Display for HostName {
851 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
852 self.0.fmt(f)
853 }
854}
855
856#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Serialize, JsonSchema)]
857pub struct EnvName(String);
858
859impl EnvName {
860 pub fn new(name: impl Into<String>) -> Option<Self> {
861 let name = name.into();
862
863 if name.contains('=') {
865 return None;
866 }
867
868 Some(Self(name))
869 }
870}
871
872impl<'de> serde::Deserialize<'de> for EnvName {
873 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
874 struct EnvNameVisitor;
875
876 impl serde::de::Visitor<'_> for EnvNameVisitor {
877 type Value = EnvName;
878
879 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
880 formatter.write_str("a string")
881 }
882
883 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
884 where
885 E: serde::de::Error,
886 {
887 let Some(name) = EnvName::new(v) else {
888 let e = "environment variable name contains a '=' character";
889 return Err(E::custom(e));
890 };
891
892 Ok(name)
893 }
894
895 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
896 where
897 E: serde::de::Error,
898 {
899 self.visit_string(v.to_string())
902 }
903 }
904
905 deserializer.deserialize_string(EnvNameVisitor)
906 }
907}
908
909impl std::ops::Deref for EnvName {
910 type Target = String;
911
912 fn deref(&self) -> &Self::Target {
913 &self.0
914 }
915}
916
917impl From<EnvName> for String {
918 fn from(name: EnvName) -> Self {
919 name.0
920 }
921}
922
923impl std::fmt::Display for EnvName {
924 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
925 self.0.fmt(f)
926 }
927}
928
929#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
930#[serde(rename_all = "kebab-case")]
931pub enum Scheduler {
932 ThreadPerHost,
933 ThreadPerCore,
934}
935
936impl FromStr for Scheduler {
937 type Err = serde_yaml::Error;
938
939 fn from_str(s: &str) -> Result<Self, Self::Err> {
940 serde_yaml::from_str(s)
941 }
942}
943
944fn default_data_directory() -> Option<String> {
945 Some("shadow.data".into())
946}
947
948fn parse_set<T>(s: &str) -> Result<HashSet<T>, <T as FromStr>::Err>
950where
951 T: std::cmp::Eq + std::hash::Hash + FromStr,
952{
953 s.split(',').map(|x| x.trim().parse()).collect()
954}
955
956fn parse_set_str(s: &str) -> Result<HashSet<String>, <String as FromStr>::Err> {
958 parse_set(s)
959}
960
961#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
962#[serde(rename_all = "kebab-case")]
963#[repr(C)]
964pub enum QDiscMode {
965 Fifo,
966 RoundRobin,
967}
968
969impl FromStr for QDiscMode {
970 type Err = serde_yaml::Error;
971
972 fn from_str(s: &str) -> Result<Self, Self::Err> {
973 serde_yaml::from_str(s)
974 }
975}
976
977#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
978#[serde(rename_all = "kebab-case")]
979pub enum Compression {
980 Xz,
981}
982
983#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
984#[serde(deny_unknown_fields)]
985pub struct FileSource {
986 pub path: String,
988 pub compression: Option<Compression>,
990}
991
992#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
993#[serde(rename_all = "kebab-case")]
994pub enum GraphSource {
995 File(FileSource),
996 Inline(String),
997}
998
999#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1000#[serde(tag = "type", rename_all = "snake_case")]
1003pub enum GraphOptions {
1004 Gml(GraphSource),
1005 #[serde(rename = "1_gbit_switch")]
1006 OneGbitSwitch,
1007}
1008
1009#[derive(Debug, Clone, Serialize, JsonSchema)]
1010#[serde(untagged)]
1011pub enum ProcessArgs {
1012 List(Vec<String>),
1013 Str(String),
1014}
1015
1016impl<'de> serde::Deserialize<'de> for ProcessArgs {
1023 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1024 struct ProcessArgsVisitor;
1025
1026 impl<'de> serde::de::Visitor<'de> for ProcessArgsVisitor {
1027 type Value = ProcessArgs;
1028
1029 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1030 formatter.write_str("a string or a sequence of strings")
1031 }
1032
1033 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1034 where
1035 E: serde::de::Error,
1036 {
1037 Ok(Self::Value::Str(v.to_owned()))
1038 }
1039
1040 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
1041 where
1042 A: serde::de::SeqAccess<'de>,
1043 {
1044 let mut v = vec![];
1045
1046 while let Some(val) = seq.next_element()? {
1047 v.push(val);
1048 }
1049
1050 Ok(Self::Value::List(v))
1051 }
1052 }
1053
1054 deserializer.deserialize_any(ProcessArgsVisitor)
1055 }
1056}
1057
1058#[derive(Debug, Copy, Clone, Eq, PartialEq)]
1063pub struct Signal(nix::sys::signal::Signal);
1064
1065impl From<nix::sys::signal::Signal> for Signal {
1066 fn from(value: nix::sys::signal::Signal) -> Self {
1067 Self(value)
1068 }
1069}
1070
1071impl TryFrom<linux_api::signal::Signal> for Signal {
1072 type Error = <nix::sys::signal::Signal as TryFrom<i32>>::Error;
1073 fn try_from(value: linux_api::signal::Signal) -> Result<Self, Self::Error> {
1074 let signal = nix::sys::signal::Signal::try_from(value.as_i32())?;
1075 Ok(Self(signal))
1076 }
1077}
1078
1079impl serde::Serialize for Signal {
1080 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1081 where
1082 S: serde::Serializer,
1083 {
1084 serializer.serialize_str(self.0.as_str())
1085 }
1086}
1087
1088impl<'de> serde::Deserialize<'de> for Signal {
1089 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1090 struct SignalVisitor;
1091
1092 impl serde::de::Visitor<'_> for SignalVisitor {
1093 type Value = Signal;
1094
1095 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1096 formatter.write_str("a signal string (e.g. \"SIGINT\") or integer")
1097 }
1098
1099 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1100 where
1101 E: serde::de::Error,
1102 {
1103 nix::sys::signal::Signal::from_str(v)
1104 .map(Signal)
1105 .map_err(|_e| E::custom(format!("Invalid signal string: {v}")))
1106 }
1107
1108 fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
1109 where
1110 E: serde::de::Error,
1111 {
1112 let v = i32::try_from(v)
1113 .map_err(|_e| E::custom(format!("Invalid signal number: {v}")))?;
1114 nix::sys::signal::Signal::try_from(v)
1115 .map(Signal)
1116 .map_err(|_e| E::custom(format!("Invalid signal number: {v}")))
1117 }
1118
1119 fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
1120 where
1121 E: serde::de::Error,
1122 {
1123 let v = i64::try_from(v)
1124 .map_err(|_e| E::custom(format!("Invalid signal number: {v}")))?;
1125 self.visit_i64(v)
1126 }
1127 }
1128
1129 deserializer.deserialize_any(SignalVisitor)
1130 }
1131}
1132
1133impl std::fmt::Display for Signal {
1134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1135 write!(f, "{}", self.0)
1136 }
1137}
1138
1139impl JsonSchema for Signal {
1140 fn schema_name() -> Cow<'static, str> {
1141 "Signal".into()
1142 }
1143
1144 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1145 schemars::json_schema!(true)
1153 }
1154}
1155
1156impl std::ops::Deref for Signal {
1157 type Target = nix::sys::signal::Signal;
1158
1159 fn deref(&self) -> &Self::Target {
1160 &self.0
1161 }
1162}
1163
1164#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1165#[serde(rename_all = "kebab-case")]
1166pub enum StraceLoggingMode {
1167 Off,
1168 Standard,
1169 Deterministic,
1170 Long,
1171}
1172
1173impl FromStr for StraceLoggingMode {
1174 type Err = serde_yaml::Error;
1175
1176 fn from_str(s: &str) -> Result<Self, Self::Err> {
1177 serde_yaml::from_str(s)
1178 }
1179}
1180
1181#[derive(Debug, Copy, Clone, JsonSchema, Eq, PartialEq)]
1235pub enum NullableOption<T> {
1236 Value(T),
1237 Null,
1238}
1239
1240impl<T> NullableOption<T> {
1241 pub fn as_ref(&self) -> NullableOption<&T> {
1242 match self {
1243 NullableOption::Value(x) => NullableOption::Value(x),
1244 NullableOption::Null => NullableOption::Null,
1245 }
1246 }
1247
1248 pub fn as_mut(&mut self) -> NullableOption<&mut T> {
1249 match self {
1250 NullableOption::Value(x) => NullableOption::Value(x),
1251 NullableOption::Null => NullableOption::Null,
1252 }
1253 }
1254
1255 pub fn to_option(self) -> Option<T> {
1258 match self {
1259 NullableOption::Value(x) => Some(x),
1260 NullableOption::Null => None,
1261 }
1262 }
1263}
1264
1265impl<T: serde::Serialize> serde::Serialize for NullableOption<T> {
1266 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1267 match self {
1268 Self::Value(x) => Ok(T::serialize(x, serializer)?),
1270 Self::Null => serializer.serialize_none(),
1271 }
1272 }
1273}
1274
1275impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for NullableOption<T> {
1276 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1277 Ok(Self::Value(T::deserialize(deserializer)?))
1279 }
1280}
1281
1282impl<T> FromStr for NullableOption<T>
1283where
1284 T: FromStr<Err: std::fmt::Debug + std::fmt::Display>,
1285{
1286 type Err = T::Err;
1287
1288 fn from_str(s: &str) -> Result<Self, Self::Err> {
1289 match s {
1290 "null" => Ok(Self::Null),
1292 x => Ok(Self::Value(FromStr::from_str(x)?)),
1293 }
1294 }
1295}
1296
1297pub trait Flatten<T> {
1299 fn flatten(self) -> Option<T>;
1300 fn flatten_ref(&self) -> Option<&T>;
1301}
1302
1303impl<T> Flatten<T> for Option<NullableOption<T>> {
1304 fn flatten(self) -> Option<T> {
1305 self.and_then(|x| x.to_option())
1306 }
1307
1308 fn flatten_ref(&self) -> Option<&T> {
1309 self.as_ref().and_then(|x| x.as_ref().to_option())
1310 }
1311}
1312
1313fn default_args_empty() -> ProcessArgs {
1315 ProcessArgs::Str("".to_string())
1316}
1317
1318fn default_sigterm() -> Signal {
1320 Signal(nix::sys::signal::Signal::SIGTERM)
1321}
1322
1323fn default_some_time_0() -> Option<units::Time<units::TimePrefix>> {
1325 Some(units::Time::new(0, units::TimePrefix::Sec))
1326}
1327
1328fn default_some_true() -> Option<bool> {
1330 Some(true)
1331}
1332
1333fn default_some_false() -> Option<bool> {
1335 Some(false)
1336}
1337
1338fn default_some_0() -> Option<u32> {
1340 Some(0)
1341}
1342
1343fn default_some_1() -> Option<u32> {
1345 Some(1)
1346}
1347
1348fn default_some_nullable_time_1() -> Option<NullableOption<units::Time<units::TimePrefix>>> {
1350 let time = units::Time::new(1, units::TimePrefix::Sec);
1351 Some(NullableOption::Value(time))
1352}
1353
1354fn default_some_info() -> Option<LogLevel> {
1356 Some(LogLevel::Info)
1357}
1358
1359pub const ONE_GBIT_SWITCH_GRAPH: &str = r#"graph [
1361 directed 0
1362 node [
1363 id 0
1364 host_bandwidth_up "1 Gbit"
1365 host_bandwidth_down "1 Gbit"
1366 ]
1367 edge [
1368 source 0
1369 target 0
1370 latency "1 ms"
1371 packet_loss 0.0
1372 ]
1373]"#;
1374
1375fn generate_help_strs(schema: schemars::Schema) -> std::collections::HashMap<String, String> {
1377 let mut defaults = std::collections::HashMap::<String, String>::new();
1379
1380 for (name, obj) in schema.get("properties").unwrap().as_object().unwrap() {
1382 let description = obj
1383 .get("description")
1384 .map(|x| x.as_str().unwrap())
1385 .unwrap_or("");
1386
1387 let description = description.replace("\n", " ");
1398
1399 let name = name.clone();
1400
1401 match obj.get("default") {
1402 Some(default) => {
1403 let space = if !description.is_empty() { " " } else { "" };
1404 defaults.insert(name, format!("{description}{space}[default: {default}]"))
1405 }
1406 None => defaults.insert(name, description.to_string()),
1407 };
1408 }
1409
1410 defaults
1411}
1412
1413pub fn parse_string_as_args(args_str: &OsStr) -> Result<Vec<OsString>, String> {
1416 if args_str.is_empty() {
1417 return Ok(Vec::new());
1418 }
1419
1420 let args_str = CString::new(args_str.as_bytes()).unwrap();
1421
1422 let mut argc: libc::c_int = 0;
1424 let mut argv: *mut *mut libc::c_char = std::ptr::null_mut();
1425 let mut error: *mut libc::c_char = std::ptr::null_mut();
1426 let rv = unsafe { c::process_parseArgStr(args_str.as_ptr(), &mut argc, &mut argv, &mut error) };
1427
1428 if !rv {
1430 let error_message = match error.is_null() {
1431 false => unsafe { CStr::from_ptr(error) }.to_str().unwrap(),
1432 true => "Unknown parsing error",
1433 }
1434 .to_string();
1435
1436 unsafe { c::process_parseArgStrFree(argv, error) };
1437 return Err(error_message);
1438 }
1439
1440 assert!(!argv.is_null());
1441
1442 let args: Vec<_> = (0..argc)
1444 .map(|x| unsafe {
1445 let arg_ptr = *argv.add(x as usize);
1446 assert!(!arg_ptr.is_null());
1447 OsStr::from_bytes(CStr::from_ptr(arg_ptr).to_bytes()).to_os_string()
1448 })
1449 .collect();
1450
1451 unsafe { c::process_parseArgStrFree(argv, error) };
1452 Ok(args)
1453}
1454
1455#[cfg(test)]
1456mod tests {
1457 use super::*;
1458
1459 #[test]
1460 #[cfg_attr(miri, ignore)]
1462 fn test_parse_args() {
1463 let arg_str = r#"the quick brown fox "jumped over" the "\"lazy\" dog""#;
1464 let expected_args = &[
1465 "the",
1466 "quick",
1467 "brown",
1468 "fox",
1469 "jumped over",
1470 "the",
1471 "\"lazy\" dog",
1472 ];
1473
1474 let arg_str: OsString = arg_str.into();
1475 let args = parse_string_as_args(&arg_str).unwrap();
1476
1477 assert_eq!(args, expected_args);
1478 }
1479
1480 #[test]
1481 #[cfg_attr(miri, ignore)]
1483 fn test_parse_args_empty() {
1484 let arg_str = "";
1485 let expected_args: &[&str] = &[];
1486
1487 let arg_str: OsString = arg_str.into();
1488 let args = parse_string_as_args(&arg_str).unwrap();
1489
1490 assert_eq!(args, expected_args);
1491 }
1492
1493 #[test]
1494 #[cfg_attr(miri, ignore)]
1496 fn test_parse_args_error() {
1497 let arg_str = r#"hello "world"#;
1498
1499 let arg_str: OsString = arg_str.into();
1500 let err_str = parse_string_as_args(&arg_str).unwrap_err();
1501
1502 assert!(!err_str.is_empty());
1503 }
1504
1505 #[test]
1506 #[cfg_attr(miri, ignore)]
1508 fn test_nullable_option() {
1509 let yaml_fmt_fn = |option| {
1511 format!(
1512 r#"
1513 general:
1514 stop_time: 1 min
1515 {option}
1516 network:
1517 graph:
1518 type: 1_gbit_switch
1519 hosts:
1520 myhost:
1521 network_node_id: 0
1522 processes:
1523 - path: /bin/true
1524 "#,
1525 )
1526 };
1527
1528 let time_1_sec = units::Time::new(1, units::TimePrefix::Sec);
1529 let time_5_sec = units::Time::new(5, units::TimePrefix::Sec);
1530
1531 let yaml = yaml_fmt_fn("heartbeat_interval: null");
1533 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1534 let cli: CliOptions = CliOptions::try_parse_from(["shadow", "-"]).unwrap();
1535
1536 let merged = ConfigOptions::new(config_file, cli);
1537 assert_eq!(merged.general.heartbeat_interval, None);
1538
1539 let yaml = yaml_fmt_fn("heartbeat_interval: null");
1541 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1542 let cli: CliOptions =
1543 CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "5s", "-"]).unwrap();
1544
1545 let merged = ConfigOptions::new(config_file, cli);
1546 assert_eq!(
1547 merged.general.heartbeat_interval,
1548 Some(NullableOption::Value(time_5_sec))
1549 );
1550
1551 let yaml = yaml_fmt_fn("heartbeat_interval: null");
1553 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1554 let cli: CliOptions =
1555 CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "null", "-"]).unwrap();
1556
1557 let merged = ConfigOptions::new(config_file, cli);
1558 assert_eq!(
1559 merged.general.heartbeat_interval,
1560 Some(NullableOption::Null)
1561 );
1562
1563 let yaml = yaml_fmt_fn("heartbeat_interval: 5s");
1565 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1566 let cli: CliOptions = CliOptions::try_parse_from(["shadow", "-"]).unwrap();
1567
1568 let merged = ConfigOptions::new(config_file, cli);
1569 assert_eq!(
1570 merged.general.heartbeat_interval,
1571 Some(NullableOption::Value(time_5_sec))
1572 );
1573
1574 let yaml = yaml_fmt_fn("heartbeat_interval: 5s");
1576 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1577 let cli: CliOptions =
1578 CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "5s", "-"]).unwrap();
1579
1580 let merged = ConfigOptions::new(config_file, cli);
1581 assert_eq!(
1582 merged.general.heartbeat_interval,
1583 Some(NullableOption::Value(time_5_sec))
1584 );
1585
1586 let yaml = yaml_fmt_fn("heartbeat_interval: 5s");
1588 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1589 let cli: CliOptions =
1590 CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "null", "-"]).unwrap();
1591
1592 let merged = ConfigOptions::new(config_file, cli);
1593 assert_eq!(
1594 merged.general.heartbeat_interval,
1595 Some(NullableOption::Null)
1596 );
1597
1598 let yaml = yaml_fmt_fn("");
1600 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1601 let cli: CliOptions = CliOptions::try_parse_from(["shadow", "-"]).unwrap();
1602
1603 let merged = ConfigOptions::new(config_file, cli);
1604 assert_eq!(
1605 merged.general.heartbeat_interval,
1606 Some(NullableOption::Value(time_1_sec))
1607 );
1608
1609 let yaml = yaml_fmt_fn("");
1611 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1612 let cli: CliOptions =
1613 CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "5s", "-"]).unwrap();
1614
1615 let merged = ConfigOptions::new(config_file, cli);
1616 assert_eq!(
1617 merged.general.heartbeat_interval,
1618 Some(NullableOption::Value(time_5_sec))
1619 );
1620
1621 let yaml = yaml_fmt_fn("");
1623 let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1624 let cli: CliOptions =
1625 CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "null", "-"]).unwrap();
1626
1627 let merged = ConfigOptions::new(config_file, cli);
1628 assert_eq!(
1629 merged.general.heartbeat_interval,
1630 Some(NullableOption::Null)
1631 );
1632 }
1633}