tracexec_core/cli/
args.rs

1use std::{
2  borrow::Cow,
3  num::ParseFloatError,
4};
5
6use clap::{
7  Args,
8  ValueEnum,
9};
10use color_eyre::eyre::bail;
11use enumflags2::BitFlags;
12use snafu::{
13  ResultExt,
14  Snafu,
15};
16
17use super::{
18  config::{
19    DebuggerConfig,
20    ExitHandling,
21    LogModeConfig,
22    ModifierConfig,
23    PtraceConfig,
24    TuiModeConfig,
25  },
26  options::{
27    ActivePane,
28    AppLayout,
29    SeccompBpf,
30  },
31};
32use crate::{
33  breakpoint::BreakPoint,
34  cli::config::{
35    ColorLevel,
36    EnvDisplay,
37    FileDescriptorDisplay,
38  },
39  event::TracerEventDetailsKind,
40  timestamp::TimestampFormat,
41};
42
43#[derive(Args, Debug, Default, Clone)]
44pub struct PtraceArgs {
45  #[clap(long, help = "Controls whether to enable seccomp-bpf optimization, which greatly improves performance", default_value_t = SeccompBpf::Auto)]
46  pub seccomp_bpf: SeccompBpf,
47  #[clap(
48    long,
49    help = "Polling interval, in microseconds. -1(default) disables polling."
50  )]
51  pub polling_interval: Option<i64>,
52}
53
54#[derive(Args, Debug, Default, Clone)]
55pub struct ModifierArgs {
56  #[clap(long, help = "Only show successful calls", default_value_t = false)]
57  pub successful_only: bool,
58  #[clap(
59    long,
60    help = "[Experimental] Try to reproduce file descriptors in commandline. This might result in an unexecutable cmdline if pipes, sockets, etc. are involved.",
61    default_value_t = false
62  )]
63  pub fd_in_cmdline: bool,
64  #[clap(
65    long,
66    help = "[Experimental] Try to reproduce stdio in commandline. This might result in an unexecutable cmdline if pipes, sockets, etc. are involved.",
67    default_value_t = false
68  )]
69  pub stdio_in_cmdline: bool,
70  #[clap(long, help = "Resolve /proc/self/exe symlink", default_value_t = false)]
71  pub resolve_proc_self_exe: bool,
72  #[clap(
73    long,
74    help = "Do not resolve /proc/self/exe symlink",
75    default_value_t = false,
76    conflicts_with = "resolve_proc_self_exe"
77  )]
78  pub no_resolve_proc_self_exe: bool,
79  #[clap(long, help = "Hide CLOEXEC fds", default_value_t = false)]
80  pub hide_cloexec_fds: bool,
81  #[clap(
82    long,
83    help = "Do not hide CLOEXEC fds",
84    default_value_t = false,
85    conflicts_with = "hide_cloexec_fds"
86  )]
87  pub no_hide_cloexec_fds: bool,
88  #[clap(long, help = "Show timestamp information", default_value_t = false)]
89  pub timestamp: bool,
90  #[clap(
91    long,
92    help = "Do not show timestamp information",
93    default_value_t = false,
94    conflicts_with = "timestamp"
95  )]
96  pub no_timestamp: bool,
97  #[clap(
98    long,
99    help = "Set the format of inline timestamp. See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for available options."
100  )]
101  pub inline_timestamp_format: Option<TimestampFormat>,
102}
103
104impl PtraceArgs {
105  pub fn merge_config(&mut self, config: PtraceConfig) {
106    // seccomp-bpf
107    if let Some(setting) = config.seccomp_bpf
108      && self.seccomp_bpf == SeccompBpf::Auto
109    {
110      self.seccomp_bpf = setting;
111    }
112  }
113}
114
115impl ModifierArgs {
116  pub fn processed(mut self) -> Self {
117    self.stdio_in_cmdline = self.fd_in_cmdline || self.stdio_in_cmdline;
118    self.resolve_proc_self_exe = match (self.resolve_proc_self_exe, self.no_resolve_proc_self_exe) {
119      (true, false) => true,
120      (false, true) => false,
121      _ => true, // default
122    };
123    self.hide_cloexec_fds = match (self.hide_cloexec_fds, self.no_hide_cloexec_fds) {
124      (true, false) => true,
125      (false, true) => false,
126      _ => true, // default
127    };
128    self.timestamp = match (self.timestamp, self.no_timestamp) {
129      (true, false) => true,
130      (false, true) => false,
131      _ => false, // default
132    };
133    self.inline_timestamp_format = self
134      .inline_timestamp_format
135      .or_else(|| Some(TimestampFormat::try_new("%H:%M:%S").unwrap()));
136    self
137  }
138
139  pub fn merge_config(&mut self, config: ModifierConfig) {
140    // false by default flags
141    self.successful_only = self.successful_only || config.successful_only.unwrap_or_default();
142    self.fd_in_cmdline |= config.fd_in_cmdline.unwrap_or_default();
143    self.stdio_in_cmdline |= config.stdio_in_cmdline.unwrap_or_default();
144    // flags that have negation counterparts
145    if (!self.no_resolve_proc_self_exe) && (!self.resolve_proc_self_exe) {
146      self.resolve_proc_self_exe = config.resolve_proc_self_exe.unwrap_or_default();
147    }
148    if (!self.no_hide_cloexec_fds) && (!self.hide_cloexec_fds) {
149      self.hide_cloexec_fds = config.hide_cloexec_fds.unwrap_or_default();
150    }
151    if let Some(c) = config.timestamp {
152      if (!self.timestamp) && (!self.no_timestamp) {
153        self.timestamp = c.enable;
154      }
155      if self.inline_timestamp_format.is_none() {
156        self.inline_timestamp_format = c.inline_format;
157      }
158    }
159  }
160}
161
162#[derive(Args, Debug)]
163pub struct TracerEventArgs {
164  // TODO:
165  //   This isn't really compatible with logging mode
166  #[clap(
167    long,
168    help = "Set the default filter to show all events. This option can be used in combination with --filter-exclude to exclude some unwanted events.",
169    conflicts_with = "filter"
170  )]
171  pub show_all_events: bool,
172  #[clap(
173    long,
174    help = "Set the default filter for events.",
175    value_parser = tracer_event_filter_parser,
176    default_value = "warning,error,exec,tracee-exit"
177  )]
178  pub filter: BitFlags<TracerEventDetailsKind>,
179  #[clap(
180    long,
181    help = "Aside from the default filter, also include the events specified here.",
182    required = false,
183    value_parser = tracer_event_filter_parser,
184    default_value_t = BitFlags::empty()
185  )]
186  pub filter_include: BitFlags<TracerEventDetailsKind>,
187  #[clap(
188    long,
189    help = "Exclude the events specified here from the default filter.",
190    value_parser = tracer_event_filter_parser,
191    default_value_t = BitFlags::empty()
192  )]
193  pub filter_exclude: BitFlags<TracerEventDetailsKind>,
194}
195
196fn tracer_event_filter_parser(filter: &str) -> Result<BitFlags<TracerEventDetailsKind>, String> {
197  let mut result = BitFlags::empty();
198  if filter == "<empty>" {
199    return Ok(result);
200  }
201  for f in filter.split(',') {
202    let kind = TracerEventDetailsKind::from_str(f, false)?;
203    if result.contains(kind) {
204      return Err(format!(
205        "Event kind '{kind}' is already included in the filter"
206      ));
207    }
208    result |= kind;
209  }
210  Ok(result)
211}
212
213impl TracerEventArgs {
214  pub fn all() -> Self {
215    Self {
216      show_all_events: true,
217      filter: Default::default(),
218      filter_include: Default::default(),
219      filter_exclude: Default::default(),
220    }
221  }
222
223  pub fn filter(&self) -> color_eyre::Result<BitFlags<TracerEventDetailsKind>> {
224    let default_filter = if self.show_all_events {
225      BitFlags::all()
226    } else {
227      self.filter
228    };
229    if self.filter_include.intersects(self.filter_exclude) {
230      bail!("filter_include and filter_exclude cannot contain common events");
231    }
232    let mut filter = default_filter | self.filter_include;
233    filter.remove(self.filter_exclude);
234    Ok(filter)
235  }
236}
237
238#[derive(Args, Debug, Default, Clone)]
239pub struct LogModeArgs {
240  #[clap(long, help = "More colors", conflicts_with = "less_colors")]
241  pub more_colors: bool,
242  #[clap(long, help = "Less colors", conflicts_with = "more_colors")]
243  pub less_colors: bool,
244  // BEGIN ugly: https://github.com/clap-rs/clap/issues/815
245  #[clap(
246    long,
247    help = "Print commandline that (hopefully) reproduces what was executed. Note: file descriptors are not handled for now.",
248    conflicts_with_all = ["show_env", "diff_env", "show_argv", "no_show_cmdline"]
249  )]
250  pub show_cmdline: bool,
251  #[clap(
252    long,
253    help = "Don't print commandline that (hopefully) reproduces what was executed."
254  )]
255  pub no_show_cmdline: bool,
256  #[clap(
257    long,
258    help = "Try to show script interpreter indicated by shebang",
259    conflicts_with = "no_show_interpreter"
260  )]
261  pub show_interpreter: bool,
262  #[clap(
263    long,
264    help = "Do not show script interpreter indicated by shebang",
265    conflicts_with = "show_interpreter"
266  )]
267  pub no_show_interpreter: bool,
268  #[clap(
269    long,
270    help = "Set the terminal foreground process group to tracee. This option is useful when tracexec is used interactively. [default]",
271    conflicts_with = "no_foreground"
272  )]
273  pub foreground: bool,
274  #[clap(
275    long,
276    help = "Do not set the terminal foreground process group to tracee",
277    conflicts_with = "foreground"
278  )]
279  pub no_foreground: bool,
280  #[clap(
281    long,
282    help = "Diff file descriptors with the original std{in/out/err}",
283    conflicts_with = "no_diff_fd"
284  )]
285  pub diff_fd: bool,
286  #[clap(
287    long,
288    help = "Do not diff file descriptors",
289    conflicts_with = "diff_fd"
290  )]
291  pub no_diff_fd: bool,
292  #[clap(long, help = "Show file descriptors", conflicts_with = "diff_fd")]
293  pub show_fd: bool,
294  #[clap(
295    long,
296    help = "Do not show file descriptors",
297    conflicts_with = "show_fd"
298  )]
299  pub no_show_fd: bool,
300  #[clap(
301    long,
302    help = "Diff environment variables with the original environment",
303    conflicts_with = "no_diff_env",
304    conflicts_with = "show_env",
305    conflicts_with = "no_show_env"
306  )]
307  pub diff_env: bool,
308  #[clap(
309    long,
310    help = "Do not diff environment variables",
311    conflicts_with = "diff_env"
312  )]
313  pub no_diff_env: bool,
314  #[clap(
315    long,
316    help = "Show environment variables",
317    conflicts_with = "no_show_env",
318    conflicts_with = "diff_env"
319  )]
320  pub show_env: bool,
321  #[clap(
322    long,
323    help = "Do not show environment variables",
324    conflicts_with = "show_env"
325  )]
326  pub no_show_env: bool,
327  #[clap(long, help = "Show comm", conflicts_with = "no_show_comm")]
328  pub show_comm: bool,
329  #[clap(long, help = "Do not show comm", conflicts_with = "show_comm")]
330  pub no_show_comm: bool,
331  #[clap(long, help = "Show argv", conflicts_with = "no_show_argv")]
332  pub show_argv: bool,
333  #[clap(long, help = "Do not show argv", conflicts_with = "show_argv")]
334  pub no_show_argv: bool,
335  #[clap(long, help = "Show filename", conflicts_with = "no_show_filename")]
336  pub show_filename: bool,
337  #[clap(long, help = "Do not show filename", conflicts_with = "show_filename")]
338  pub no_show_filename: bool,
339  #[clap(long, help = "Show cwd", conflicts_with = "no_show_cwd")]
340  pub show_cwd: bool,
341  #[clap(long, help = "Do not show cwd", conflicts_with = "show_cwd")]
342  pub no_show_cwd: bool,
343  #[clap(long, help = "Decode errno values", conflicts_with = "no_decode_errno")]
344  pub decode_errno: bool,
345  #[clap(
346    long,
347    help = "Do not decode errno values",
348    conflicts_with = "decode_errno"
349  )]
350  pub no_decode_errno: bool,
351  // END ugly
352}
353
354impl LogModeArgs {
355  pub fn foreground(&self) -> bool {
356    match (self.foreground, self.no_foreground) {
357      (false, true) => false,
358      (true, false) => true,
359      _ => true,
360    }
361  }
362
363  pub fn merge_config(&mut self, config: LogModeConfig) {
364    /// fallback to config value if both --x and --no-x are not set
365    macro_rules! fallback {
366      ($x:ident) => {
367        ::paste::paste! {
368          if (!self.$x) && (!self.[<no_ $x>]) {
369            if let Some(x) = config.$x {
370              if x {
371                self.$x = true;
372              } else {
373                self.[<no_ $x>] = true;
374              }
375            }
376          }
377        }
378      };
379    }
380    fallback!(show_interpreter);
381    fallback!(foreground);
382    fallback!(show_comm);
383    fallback!(show_filename);
384    fallback!(show_cwd);
385    fallback!(decode_errno);
386    match config.fd_display {
387      Some(FileDescriptorDisplay::Show) => {
388        if (!self.no_show_fd) && (!self.diff_fd) {
389          self.show_fd = true;
390        }
391      }
392      Some(FileDescriptorDisplay::Diff) => {
393        if (!self.show_fd) && (!self.no_diff_fd) {
394          self.diff_fd = true;
395        }
396      }
397      Some(FileDescriptorDisplay::Hide) => {
398        if (!self.diff_fd) && (!self.show_fd) {
399          self.no_diff_fd = true;
400          self.no_show_fd = true;
401        }
402      }
403      _ => (),
404    }
405    fallback!(show_cmdline);
406    if !self.show_cmdline {
407      fallback!(show_argv);
408      tracing::warn!("{}", self.show_argv);
409      match config.env_display {
410        Some(EnvDisplay::Show) => {
411          if (!self.diff_env) && (!self.no_show_env) {
412            self.show_env = true;
413          }
414        }
415        Some(EnvDisplay::Diff) => {
416          if (!self.show_env) && (!self.no_diff_env) {
417            self.diff_env = true;
418          }
419        }
420        Some(EnvDisplay::Hide) => {
421          if (!self.show_env) && (!self.diff_env) {
422            self.no_diff_env = true;
423            self.no_show_env = true;
424          }
425        }
426        _ => (),
427      }
428    }
429    match config.color_level {
430      Some(ColorLevel::Less) => {
431        if !self.more_colors {
432          self.less_colors = true;
433        }
434      }
435      Some(ColorLevel::More) => {
436        if !self.less_colors {
437          self.more_colors = true;
438        }
439      }
440      _ => (),
441    }
442  }
443}
444
445#[derive(Args, Debug, Default, Clone)]
446pub struct TuiModeArgs {
447  #[clap(
448    long,
449    short,
450    help = "Allocate a pseudo terminal and show it alongside the TUI"
451  )]
452  pub tty: bool,
453  #[clap(long, short, help = "Keep the event list scrolled to the bottom")]
454  pub follow: bool,
455  #[clap(
456    long,
457    help = "Instead of waiting for the root child to exit, terminate when the TUI exits",
458    conflicts_with = "kill_on_exit"
459  )]
460  pub terminate_on_exit: bool,
461  #[clap(
462    long,
463    help = "Instead of waiting for the root child to exit, kill when the TUI exits"
464  )]
465  pub kill_on_exit: bool,
466  #[clap(
467    long,
468    short = 'A',
469    help = "Set the default active pane to use when TUI launches",
470    requires = "tty"
471  )]
472  pub active_pane: Option<ActivePane>,
473  #[clap(
474    long,
475    short = 'L',
476    help = "Set the layout of the TUI when it launches",
477    requires = "tty"
478  )]
479  pub layout: Option<AppLayout>,
480  #[clap(
481    long,
482    short = 'F',
483    help = "Set the frame rate of the TUI (60 by default)",
484    value_parser = frame_rate_parser
485  )]
486  pub frame_rate: Option<f64>,
487  #[clap(
488    long,
489    short = 'm',
490    help = "Max number of events to keep in TUI (0=unlimited)"
491  )]
492  pub max_events: Option<u64>,
493}
494
495#[derive(Args, Debug, Default, Clone)]
496pub struct DebuggerArgs {
497  #[clap(
498    long,
499    short = 'D',
500    help = "Set the default external command to run when using \"Detach, Stop and Run Command\" feature in Hit Manager"
501  )]
502  pub default_external_command: Option<String>,
503  #[clap(
504    long = "add-breakpoint",
505    short = 'b',
506    value_parser = breakpoint_parser,
507    help = "Add a new breakpoint to the tracer. This option can be used multiple times. The format is <syscall-stop>:<pattern-type>:<pattern>, where syscall-stop can be sysenter or sysexit, pattern-type can be argv-regex, in-filename or exact-filename. For example, sysexit:in-filename:/bash",
508  )]
509  pub breakpoints: Vec<BreakPoint>,
510}
511
512impl TuiModeArgs {
513  pub fn merge_config(&mut self, config: TuiModeConfig) {
514    self.active_pane = self.active_pane.or(config.active_pane);
515    self.layout = self.layout.or(config.layout);
516    self.frame_rate = self.frame_rate.or(config.frame_rate);
517    self.max_events = self.max_events.or(config.max_events);
518    self.follow |= config.follow.unwrap_or_default();
519    if (!self.terminate_on_exit) && (!self.kill_on_exit) {
520      match config.exit_handling {
521        Some(ExitHandling::Kill) => self.kill_on_exit = true,
522        Some(ExitHandling::Terminate) => self.terminate_on_exit = true,
523        _ => (),
524      }
525    }
526  }
527}
528
529impl DebuggerArgs {
530  pub fn merge_config(&mut self, config: DebuggerConfig) {
531    if self.default_external_command.is_none() {
532      self.default_external_command = config.default_external_command;
533    }
534  }
535}
536
537fn frame_rate_parser(s: &str) -> Result<f64, ParseFrameRateError> {
538  let v = s.parse::<f64>().with_context(|_| ParseFloatSnafu {
539    value: s.to_string(),
540  })?;
541  if v < 0.0 || v.is_nan() || v.is_infinite() {
542    Err(ParseFrameRateError::Invalid)
543  } else if v < 5.0 {
544    Err(ParseFrameRateError::TooLow)
545  } else {
546    Ok(v)
547  }
548}
549
550fn breakpoint_parser(s: &str) -> Result<BreakPoint, Cow<'static, str>> {
551  BreakPoint::try_from(s)
552}
553
554#[derive(Snafu, Debug)]
555enum ParseFrameRateError {
556  #[snafu(display("Failed to parse frame rate {value} as a floating point number"))]
557  ParseFloat {
558    source: ParseFloatError,
559    value: String,
560  },
561  #[snafu(display("Invalid frame rate"))]
562  Invalid,
563  #[snafu(display("Frame rate too low, must be at least 5.0"))]
564  TooLow,
565}
566
567#[derive(Args, Debug, Default, Clone)]
568pub struct ExporterArgs {
569  #[clap(short, long, help = "prettify the output if supported")]
570  pub pretty: bool,
571}
572
573#[cfg(test)]
574mod tests {
575  use clap::Parser;
576
577  use super::*;
578
579  // Helper wrapper so we can test clap parsing easily
580  #[derive(Parser, Debug)]
581  struct TestCli<T: Args + Clone + std::fmt::Debug> {
582    #[clap(flatten)]
583    args: T,
584  }
585
586  /* ----------------------------- PtraceArgs ----------------------------- */
587
588  #[test]
589  fn test_ptrace_args_merge_config() {
590    let mut args = PtraceArgs {
591      seccomp_bpf: SeccompBpf::Auto,
592      polling_interval: None,
593    };
594
595    let cfg = PtraceConfig {
596      seccomp_bpf: Some(SeccompBpf::On),
597    };
598
599    args.merge_config(cfg);
600    assert_eq!(args.seccomp_bpf, SeccompBpf::On);
601  }
602
603  #[test]
604  fn test_ptrace_args_cli_parse() {
605    let cli = TestCli::<PtraceArgs>::parse_from(["test", "--polling-interval", "100"]);
606    assert_eq!(cli.args.polling_interval, Some(100));
607  }
608
609  /* ---------------------------- ModifierArgs ----------------------------- */
610
611  #[test]
612  fn test_modifier_processed_defaults() {
613    let args = ModifierArgs::default().processed();
614    assert!(args.resolve_proc_self_exe);
615    assert!(args.hide_cloexec_fds);
616    assert!(!args.timestamp);
617    assert!(args.inline_timestamp_format.is_some());
618  }
619
620  #[test]
621  fn test_modifier_processed_fd_implies_stdio() {
622    let args = ModifierArgs {
623      fd_in_cmdline: true,
624      ..Default::default()
625    }
626    .processed();
627
628    assert!(args.stdio_in_cmdline);
629  }
630
631  #[test]
632  fn test_modifier_merge_config() {
633    let mut args = ModifierArgs::default();
634
635    let cfg = ModifierConfig {
636      successful_only: Some(true),
637      fd_in_cmdline: Some(true),
638      stdio_in_cmdline: None,
639      resolve_proc_self_exe: Some(false),
640      hide_cloexec_fds: Some(false),
641      timestamp: None,
642      seccomp_bpf: None,
643    };
644
645    args.merge_config(cfg);
646
647    assert!(args.successful_only);
648    assert!(args.fd_in_cmdline);
649    assert!(!args.resolve_proc_self_exe);
650    assert!(!args.hide_cloexec_fds);
651  }
652
653  #[test]
654  fn test_modifier_args_cli_overrides_config_positive() {
655    let mut args = ModifierArgs {
656      resolve_proc_self_exe: true, // CLI explicitly enables
657      ..Default::default()
658    };
659
660    let cfg = ModifierConfig {
661      resolve_proc_self_exe: Some(false),
662      ..Default::default()
663    };
664
665    args.merge_config(cfg);
666
667    assert!(args.resolve_proc_self_exe);
668  }
669
670  #[test]
671  fn test_modifier_args_cli_no_flag_blocks_config() {
672    let mut args = ModifierArgs {
673      no_hide_cloexec_fds: true, // CLI explicitly disables
674      ..Default::default()
675    };
676
677    let cfg = ModifierConfig {
678      hide_cloexec_fds: Some(true),
679      ..Default::default()
680    };
681
682    args.merge_config(cfg);
683
684    assert!(!args.hide_cloexec_fds);
685  }
686
687  #[test]
688  fn test_modifier_cli_parse_conflicts() {
689    let cli = TestCli::<ModifierArgs>::parse_from(["test", "--no-timestamp"]);
690
691    let processed = cli.args.processed();
692    assert!(!processed.timestamp);
693  }
694
695  #[test]
696  fn test_modifier_args_timestamp_cli_overrides_config() {
697    let mut args = ModifierArgs {
698      timestamp: true,
699      ..Default::default()
700    };
701
702    let cfg = ModifierConfig {
703      timestamp: Some(crate::cli::config::TimestampConfig {
704        enable: false,
705        inline_format: None,
706      }),
707      ..Default::default()
708    };
709
710    args.merge_config(cfg);
711
712    assert!(args.timestamp);
713  }
714
715  /* -------------------------- TracerEventArgs ---------------------------- */
716
717  #[test]
718  fn test_tracer_event_filter_parser_basic() {
719    let f = tracer_event_filter_parser("warning,error").unwrap();
720    assert!(f.contains(TracerEventDetailsKind::Warning));
721    assert!(f.contains(TracerEventDetailsKind::Error));
722  }
723
724  #[test]
725  fn test_tracer_event_filter_duplicate() {
726    let err = tracer_event_filter_parser("warning,warning").unwrap_err();
727    assert!(err.contains("already included"));
728  }
729
730  #[test]
731  fn test_tracer_event_args_all() {
732    let args = TracerEventArgs::all();
733    let f = args.filter().unwrap();
734    assert_eq!(f, BitFlags::all());
735  }
736
737  #[test]
738  fn test_tracer_event_include_exclude_conflict() {
739    let args = TracerEventArgs {
740      show_all_events: false,
741      filter: BitFlags::empty(),
742      filter_include: TracerEventDetailsKind::Error.into(),
743      filter_exclude: TracerEventDetailsKind::Error.into(),
744    };
745
746    assert!(args.filter().is_err());
747  }
748
749  /* ---------------------------- LogModeArgs ------------------------------ */
750
751  #[test]
752  fn test_logmode_foreground_logic() {
753    let args = LogModeArgs {
754      foreground: false,
755      no_foreground: true,
756      ..Default::default()
757    };
758    assert!(!args.foreground());
759
760    let args = LogModeArgs {
761      foreground: true,
762      no_foreground: false,
763      ..Default::default()
764    };
765    assert!(args.foreground());
766  }
767
768  #[test]
769  fn test_logmode_merge_color_config() {
770    let mut args = LogModeArgs::default();
771
772    let cfg = LogModeConfig {
773      color_level: Some(ColorLevel::Less),
774      ..Default::default()
775    };
776
777    args.merge_config(cfg);
778    assert!(args.less_colors);
779  }
780
781  #[test]
782  fn test_logmode_fd_display_config() {
783    let mut args = LogModeArgs::default();
784
785    let cfg = LogModeConfig {
786      fd_display: Some(FileDescriptorDisplay::Show),
787      ..Default::default()
788    };
789
790    args.merge_config(cfg);
791    assert!(args.show_fd);
792  }
793
794  #[test]
795  fn test_logmode_cli_parse() {
796    let cli = TestCli::<LogModeArgs>::parse_from(["test", "--show-cmdline", "--show-interpreter"]);
797
798    assert!(cli.args.show_cmdline);
799    assert!(cli.args.show_interpreter);
800  }
801
802  #[test]
803  fn test_logmode_cli_no_foreground_overrides_config() {
804    let mut args = LogModeArgs {
805      no_foreground: true,
806      ..Default::default()
807    };
808
809    let cfg = LogModeConfig {
810      foreground: Some(true),
811      ..Default::default()
812    };
813
814    args.merge_config(cfg);
815
816    assert!(!args.foreground());
817  }
818
819  #[test]
820  fn test_logmode_cli_show_fd_overrides_config_hide() {
821    let mut args = LogModeArgs {
822      show_fd: true,
823      ..Default::default()
824    };
825
826    let cfg = LogModeConfig {
827      fd_display: Some(FileDescriptorDisplay::Hide),
828      ..Default::default()
829    };
830
831    args.merge_config(cfg);
832
833    assert!(args.show_fd);
834    assert!(!args.no_show_fd);
835  }
836
837  #[test]
838  fn test_logmode_cli_color_overrides_config() {
839    let mut args = LogModeArgs {
840      more_colors: true,
841      ..Default::default()
842    };
843
844    let cfg = LogModeConfig {
845      color_level: Some(ColorLevel::Less),
846      ..Default::default()
847    };
848
849    args.merge_config(cfg);
850
851    assert!(args.more_colors);
852    assert!(!args.less_colors);
853  }
854
855  /* ----------------------------- TuiModeArgs ----------------------------- */
856
857  #[test]
858  fn test_tui_merge_config_exit_handling() {
859    let mut args = TuiModeArgs::default();
860
861    let cfg = TuiModeConfig {
862      exit_handling: Some(ExitHandling::Kill),
863      follow: Some(true),
864      ..Default::default()
865    };
866
867    args.merge_config(cfg);
868
869    assert!(args.kill_on_exit);
870    assert!(args.follow);
871  }
872
873  #[test]
874  fn test_tui_cli_parse() {
875    let cli =
876      TestCli::<TuiModeArgs>::parse_from(["test", "--tty", "--follow", "--frame-rate", "30"]);
877
878    assert!(cli.args.tty);
879    assert!(cli.args.follow);
880    assert_eq!(cli.args.frame_rate, Some(30.0));
881  }
882
883  #[test]
884  fn test_tui_cli_exit_handling_overrides_config() {
885    let mut args = TuiModeArgs {
886      terminate_on_exit: true,
887      ..Default::default()
888    };
889
890    let cfg = TuiModeConfig {
891      exit_handling: Some(ExitHandling::Kill),
892      ..Default::default()
893    };
894
895    args.merge_config(cfg);
896
897    assert!(args.terminate_on_exit);
898    assert!(!args.kill_on_exit);
899  }
900
901  /* --------------------------- DebuggerArgs ------------------------------ */
902
903  #[test]
904  fn test_debugger_merge_config() {
905    let mut args = DebuggerArgs::default();
906
907    let cfg = DebuggerConfig {
908      default_external_command: Some("echo hi".into()),
909    };
910
911    args.merge_config(cfg);
912    assert_eq!(args.default_external_command.as_deref(), Some("echo hi"));
913  }
914
915  #[test]
916  fn test_debugger_cli_parse_breakpoint() {
917    let cli = TestCli::<DebuggerArgs>::parse_from([
918      "test",
919      "--add-breakpoint",
920      "sysenter:exact-filename:/bin/ls",
921    ]);
922
923    assert_eq!(cli.args.breakpoints.len(), 1);
924  }
925
926  #[test]
927  fn test_debugger_cli_command_overrides_config() {
928    let mut args = DebuggerArgs {
929      default_external_command: Some("cli-cmd".into()),
930      ..Default::default()
931    };
932
933    let cfg = DebuggerConfig {
934      default_external_command: Some("config-cmd".into()),
935    };
936
937    args.merge_config(cfg);
938
939    assert_eq!(args.default_external_command.as_deref(), Some("cli-cmd"));
940  }
941
942  /* ------------------------- frame_rate_parser --------------------------- */
943
944  #[test]
945  fn test_frame_rate_parser_valid() {
946    assert_eq!(frame_rate_parser("60").unwrap(), 60.0);
947  }
948
949  #[test]
950  fn test_frame_rate_parser_too_low() {
951    let err = frame_rate_parser("1").unwrap_err();
952    let msg = err.to_string();
953    assert!(msg.contains("too low"));
954  }
955
956  #[test]
957  fn test_frame_rate_parser_invalid() {
958    let err = frame_rate_parser("-1").unwrap_err();
959    assert!(err.to_string().contains("Invalid"));
960  }
961
962  /* ----------------------------- ExporterArgs ---------------------------- */
963
964  #[test]
965  fn test_exporter_cli_parse() {
966    let cli = TestCli::<ExporterArgs>::parse_from(["test", "--pretty"]);
967
968    assert!(cli.args.pretty);
969  }
970}