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 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, };
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, };
128 self.timestamp = match (self.timestamp, self.no_timestamp) {
129 (true, false) => true,
130 (false, true) => false,
131 _ => false, };
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 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 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 #[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 #[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 }
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 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 #[derive(Parser, Debug)]
581 struct TestCli<T: Args + Clone + std::fmt::Debug> {
582 #[clap(flatten)]
583 args: T,
584 }
585
586 #[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 #[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, ..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, ..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 #[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 #[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 #[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 #[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 #[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 #[test]
965 fn test_exporter_cli_parse() {
966 let cli = TestCli::<ExporterArgs>::parse_from(["test", "--pretty"]);
967
968 assert!(cli.args.pretty);
969 }
970}