1use std::{
2 borrow::Cow,
3 fmt,
4 num::ParseFloatError,
5 path::{
6 Path,
7 PathBuf,
8 },
9};
10
11use clap::{
12 Args,
13 ValueEnum,
14};
15use color_eyre::eyre::bail;
16use enumflags2::BitFlags;
17use snafu::{
18 ResultExt,
19 Snafu,
20};
21
22use super::{
23 config::{
24 DebuggerConfig,
25 ExitHandling,
26 LogModeConfig,
27 ModifierConfig,
28 PtraceConfig,
29 TuiModeConfig,
30 },
31 keys::TuiKeyBindingsConfig,
32 options::{
33 ActivePane,
34 AppLayout,
35 SeccompBpf,
36 },
37};
38use crate::{
39 breakpoint::BreakPoint,
40 cli::config::{
41 ColorLevel,
42 EnvDisplay,
43 FileDescriptorDisplay,
44 },
45 event::TracerEventDetailsKind,
46 timestamp::TimestampFormat,
47};
48
49#[derive(Args, Debug, Default, Clone)]
50pub struct PtraceArgs {
51 #[clap(long, help = "Controls whether to enable seccomp-bpf optimization, which greatly improves performance", default_value_t = SeccompBpf::Auto)]
52 pub seccomp_bpf: SeccompBpf,
53 #[clap(
54 long,
55 help = "Polling interval, in microseconds. -1(default) disables polling."
56 )]
57 pub polling_interval: Option<i64>,
58}
59
60#[derive(Args, Debug, Default, Clone)]
61pub struct ModifierArgs {
62 #[clap(long, help = "Only show successful calls", default_value_t = false)]
63 pub successful_only: bool,
64 #[clap(
65 long,
66 help = "[Experimental] Try to reproduce file descriptors in commandline. This might result in an unexecutable cmdline if pipes, sockets, etc. are involved.",
67 default_value_t = false
68 )]
69 pub fd_in_cmdline: bool,
70 #[clap(
71 long,
72 help = "[Experimental] Try to reproduce stdio in commandline. This might result in an unexecutable cmdline if pipes, sockets, etc. are involved.",
73 default_value_t = false
74 )]
75 pub stdio_in_cmdline: bool,
76 #[clap(long, help = "Resolve /proc/self/exe symlink", default_value_t = false)]
77 pub resolve_proc_self_exe: bool,
78 #[clap(
79 long,
80 help = "Do not resolve /proc/self/exe symlink",
81 default_value_t = false,
82 conflicts_with = "resolve_proc_self_exe"
83 )]
84 pub no_resolve_proc_self_exe: bool,
85 #[clap(long, help = "Hide CLOEXEC fds", default_value_t = false)]
86 pub hide_cloexec_fds: bool,
87 #[clap(
88 long,
89 help = "Do not hide CLOEXEC fds",
90 default_value_t = false,
91 conflicts_with = "hide_cloexec_fds"
92 )]
93 pub no_hide_cloexec_fds: bool,
94 #[clap(long, help = "Show timestamp information", default_value_t = false)]
95 pub timestamp: bool,
96 #[clap(
97 long,
98 help = "Do not show timestamp information",
99 default_value_t = false,
100 conflicts_with = "timestamp"
101 )]
102 pub no_timestamp: bool,
103 #[clap(
104 long,
105 help = "Set the format of inline timestamp. See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for available options."
106 )]
107 pub inline_timestamp_format: Option<TimestampFormat>,
108 #[clap(long, help = "Collect cgroup information", default_value_t = false)]
109 pub collect_cgroup: bool,
110 #[clap(
111 long,
112 help = "Do not collect cgroup information",
113 default_value_t = false,
114 conflicts_with = "collect_cgroup"
115 )]
116 pub no_collect_cgroup: bool,
117}
118
119impl PtraceArgs {
120 pub fn merge_config(&mut self, config: PtraceConfig) {
121 if let Some(setting) = config.seccomp_bpf
123 && self.seccomp_bpf == SeccompBpf::Auto
124 {
125 self.seccomp_bpf = setting;
126 }
127 }
128}
129
130impl ModifierArgs {
131 pub fn processed(mut self) -> Self {
132 self.stdio_in_cmdline = self.fd_in_cmdline || self.stdio_in_cmdline;
133 self.resolve_proc_self_exe = match (self.resolve_proc_self_exe, self.no_resolve_proc_self_exe) {
134 (true, false) => true,
135 (false, true) => false,
136 _ => true, };
138 self.hide_cloexec_fds = match (self.hide_cloexec_fds, self.no_hide_cloexec_fds) {
139 (true, false) => true,
140 (false, true) => false,
141 _ => true, };
143 self.timestamp = match (self.timestamp, self.no_timestamp) {
144 (true, false) => true,
145 (false, true) => false,
146 _ => false, };
148 self.collect_cgroup = match (self.collect_cgroup, self.no_collect_cgroup) {
149 (true, false) => true,
150 (false, true) => false,
151 _ => false, };
153 self
154 .inline_timestamp_format
155 .get_or_insert_with(TimestampFormat::default);
156 self
157 }
158
159 pub fn merge_config(&mut self, config: ModifierConfig) {
160 self.successful_only = self.successful_only || config.successful_only.unwrap_or_default();
162 self.fd_in_cmdline |= config.fd_in_cmdline.unwrap_or_default();
163 self.stdio_in_cmdline |= config.stdio_in_cmdline.unwrap_or_default();
164 if (!self.no_resolve_proc_self_exe) && (!self.resolve_proc_self_exe) {
166 self.resolve_proc_self_exe = config.resolve_proc_self_exe.unwrap_or_default();
167 }
168 if (!self.no_hide_cloexec_fds) && (!self.hide_cloexec_fds) {
169 self.hide_cloexec_fds = config.hide_cloexec_fds.unwrap_or_default();
170 }
171 if let Some(c) = config.timestamp {
172 if (!self.timestamp) && (!self.no_timestamp) {
173 self.timestamp = c.enable;
174 }
175 if self.inline_timestamp_format.is_none() {
176 self.inline_timestamp_format = c.inline_format;
177 }
178 }
179 if (!self.no_collect_cgroup) && (!self.collect_cgroup) {
180 self.collect_cgroup = config.collect_cgroup.unwrap_or_default();
181 }
182 }
183}
184
185#[derive(Args, Debug)]
186pub struct TracerEventArgs {
187 #[clap(
190 long,
191 help = "Set the default filter to show all events. This option can be used in combination with --filter-exclude to exclude some unwanted events.",
192 conflicts_with = "filter"
193 )]
194 pub show_all_events: bool,
195 #[clap(
196 long,
197 help = "Set the default filter for events.",
198 value_parser = tracer_event_filter_parser,
199 default_value = "warning,error,exec,tracee-exit"
200 )]
201 pub filter: BitFlags<TracerEventDetailsKind>,
202 #[clap(
203 long,
204 help = "Aside from the default filter, also include the events specified here.",
205 required = false,
206 value_parser = tracer_event_filter_parser,
207 default_value_t = BitFlags::empty()
208 )]
209 pub filter_include: BitFlags<TracerEventDetailsKind>,
210 #[clap(
211 long,
212 help = "Exclude the events specified here from the default filter.",
213 value_parser = tracer_event_filter_parser,
214 default_value_t = BitFlags::empty()
215 )]
216 pub filter_exclude: BitFlags<TracerEventDetailsKind>,
217}
218
219fn tracer_event_filter_parser(filter: &str) -> Result<BitFlags<TracerEventDetailsKind>, String> {
220 let mut result = BitFlags::empty();
221 if filter == "<empty>" {
222 return Ok(result);
223 }
224 for f in filter.split(',') {
225 let kind = TracerEventDetailsKind::from_str(f, false)?;
226 if result.contains(kind) {
227 return Err(format!(
228 "Event kind '{kind}' is already included in the filter"
229 ));
230 }
231 result |= kind;
232 }
233 Ok(result)
234}
235
236impl TracerEventArgs {
237 pub fn all() -> Self {
238 Self {
239 show_all_events: true,
240 filter: Default::default(),
241 filter_include: Default::default(),
242 filter_exclude: Default::default(),
243 }
244 }
245
246 pub fn filter(&self) -> color_eyre::Result<BitFlags<TracerEventDetailsKind>> {
247 let default_filter = if self.show_all_events {
248 BitFlags::all()
249 } else {
250 self.filter
251 };
252 if self.filter_include.intersects(self.filter_exclude) {
253 bail!("filter_include and filter_exclude cannot contain common events");
254 }
255 let mut filter = default_filter | self.filter_include;
256 filter.remove(self.filter_exclude);
257 Ok(filter)
258 }
259}
260
261#[derive(Args, Debug, Default, Clone)]
262pub struct LogModeArgs {
263 #[clap(long, help = "More colors", conflicts_with = "less_colors")]
264 pub more_colors: bool,
265 #[clap(long, help = "Less colors", conflicts_with = "more_colors")]
266 pub less_colors: bool,
267 #[clap(
269 long,
270 help = "Print commandline that (hopefully) reproduces what was executed. Note: file descriptors are not handled for now.",
271 conflicts_with_all = ["show_env", "diff_env", "show_argv", "no_show_cmdline"]
272 )]
273 pub show_cmdline: bool,
274 #[clap(
275 long,
276 help = "Don't print commandline that (hopefully) reproduces what was executed."
277 )]
278 pub no_show_cmdline: bool,
279 #[clap(
280 long,
281 help = "Try to show script interpreter indicated by shebang",
282 conflicts_with = "no_show_interpreter"
283 )]
284 pub show_interpreter: bool,
285 #[clap(
286 long,
287 help = "Do not show script interpreter indicated by shebang",
288 conflicts_with = "show_interpreter"
289 )]
290 pub no_show_interpreter: bool,
291 #[clap(
292 long,
293 help = "Set the terminal foreground process group to tracee. This option is useful when tracexec is used interactively. [default]",
294 conflicts_with = "no_foreground"
295 )]
296 pub foreground: bool,
297 #[clap(
298 long,
299 help = "Do not set the terminal foreground process group to tracee",
300 conflicts_with = "foreground"
301 )]
302 pub no_foreground: bool,
303 #[clap(
304 long,
305 help = "Diff file descriptors with the original std{in/out/err}",
306 conflicts_with = "no_diff_fd"
307 )]
308 pub diff_fd: bool,
309 #[clap(
310 long,
311 help = "Do not diff file descriptors",
312 conflicts_with = "diff_fd"
313 )]
314 pub no_diff_fd: bool,
315 #[clap(long, help = "Show file descriptors", conflicts_with = "diff_fd")]
316 pub show_fd: bool,
317 #[clap(
318 long,
319 help = "Do not show file descriptors",
320 conflicts_with = "show_fd"
321 )]
322 pub no_show_fd: bool,
323 #[clap(
324 long,
325 help = "Diff environment variables with the original environment",
326 conflicts_with = "no_diff_env",
327 conflicts_with = "show_env",
328 conflicts_with = "no_show_env"
329 )]
330 pub diff_env: bool,
331 #[clap(
332 long,
333 help = "Do not diff environment variables",
334 conflicts_with = "diff_env"
335 )]
336 pub no_diff_env: bool,
337 #[clap(
338 long,
339 help = "Show environment variables",
340 conflicts_with = "no_show_env",
341 conflicts_with = "diff_env"
342 )]
343 pub show_env: bool,
344 #[clap(
345 long,
346 help = "Do not show environment variables",
347 conflicts_with = "show_env"
348 )]
349 pub no_show_env: bool,
350 #[clap(long, help = "Show comm", conflicts_with = "no_show_comm")]
351 pub show_comm: bool,
352 #[clap(long, help = "Do not show comm", conflicts_with = "show_comm")]
353 pub no_show_comm: bool,
354 #[clap(long, help = "Show argv", conflicts_with = "no_show_argv")]
355 pub show_argv: bool,
356 #[clap(long, help = "Do not show argv", conflicts_with = "show_argv")]
357 pub no_show_argv: bool,
358 #[clap(long, help = "Show filename", conflicts_with = "no_show_filename")]
359 pub show_filename: bool,
360 #[clap(long, help = "Do not show filename", conflicts_with = "show_filename")]
361 pub no_show_filename: bool,
362 #[clap(long, help = "Show cwd", conflicts_with = "no_show_cwd")]
363 pub show_cwd: bool,
364 #[clap(long, help = "Do not show cwd", conflicts_with = "show_cwd")]
365 pub no_show_cwd: bool,
366 #[clap(long, help = "Decode errno values", conflicts_with = "no_decode_errno")]
367 pub decode_errno: bool,
368 #[clap(
369 long,
370 help = "Do not decode errno values",
371 conflicts_with = "decode_errno"
372 )]
373 pub no_decode_errno: bool,
374 }
376
377impl LogModeArgs {
378 pub fn foreground(&self) -> bool {
379 match (self.foreground, self.no_foreground) {
380 (false, true) => false,
381 (true, false) => true,
382 _ => true,
383 }
384 }
385
386 pub fn merge_config(&mut self, config: LogModeConfig) {
387 macro_rules! fallback {
389 ($x:ident) => {
390 ::paste::paste! {
391 if (!self.$x) && (!self.[<no_ $x>]) {
392 if let Some(x) = config.$x {
393 if x {
394 self.$x = true;
395 } else {
396 self.[<no_ $x>] = true;
397 }
398 }
399 }
400 }
401 };
402 }
403 fallback!(show_interpreter);
404 fallback!(foreground);
405 fallback!(show_comm);
406 fallback!(show_filename);
407 fallback!(show_cwd);
408 fallback!(decode_errno);
409 match config.fd_display {
410 Some(FileDescriptorDisplay::Show) => {
411 if (!self.no_show_fd) && (!self.diff_fd) {
412 self.show_fd = true;
413 }
414 }
415 Some(FileDescriptorDisplay::Diff) => {
416 if (!self.show_fd) && (!self.no_diff_fd) {
417 self.diff_fd = true;
418 }
419 }
420 Some(FileDescriptorDisplay::Hide) if (!self.diff_fd) && (!self.show_fd) => {
421 self.no_diff_fd = true;
422 self.no_show_fd = true;
423 }
424 _ => (),
425 }
426 fallback!(show_cmdline);
427 if !self.show_cmdline {
428 fallback!(show_argv);
429 tracing::warn!("{}", self.show_argv);
430 match config.env_display {
431 Some(EnvDisplay::Show) => {
432 if (!self.diff_env) && (!self.no_show_env) {
433 self.show_env = true;
434 }
435 }
436 Some(EnvDisplay::Diff) => {
437 if (!self.show_env) && (!self.no_diff_env) {
438 self.diff_env = true;
439 }
440 }
441 Some(EnvDisplay::Hide) if (!self.show_env) && (!self.diff_env) => {
442 self.no_diff_env = true;
443 self.no_show_env = true;
444 }
445 _ => (),
446 }
447 }
448 match config.color_level {
449 Some(ColorLevel::Less) => {
450 if !self.more_colors {
451 self.less_colors = true;
452 }
453 }
454 Some(ColorLevel::More) if !self.less_colors => {
455 self.more_colors = true;
456 }
457 _ => (),
458 }
459 }
460}
461
462#[derive(Args, Debug, Default, Clone)]
463pub struct TuiModeArgs {
464 #[clap(
465 long,
466 help = "Do not allocate a pseudo terminal; redirect stdin/out/err to /dev/null"
467 )]
468 pub no_tty: bool,
469 #[clap(long, short, help = "Keep the event list scrolled to the bottom")]
470 pub follow: bool,
471 #[clap(
472 long,
473 help = "Instead of waiting for the root child to exit, terminate when the TUI exits",
474 conflicts_with = "kill_on_exit"
475 )]
476 pub terminate_on_exit: bool,
477 #[clap(
478 long,
479 help = "Instead of waiting for the root child to exit, kill when the TUI exits"
480 )]
481 pub kill_on_exit: bool,
482 #[clap(
483 long,
484 short = 'A',
485 help = "Set the default active pane to use when TUI launches",
486 conflicts_with = "no_tty"
487 )]
488 pub active_pane: Option<ActivePane>,
489 #[clap(
490 long,
491 short = 'L',
492 help = "Set the layout of the TUI when it launches",
493 conflicts_with = "no_tty"
494 )]
495 pub layout: Option<AppLayout>,
496 #[clap(
497 long,
498 short = 'F',
499 help = "Set the frame rate of the TUI (60 by default)",
500 value_parser = frame_rate_parser
501 )]
502 pub frame_rate: Option<f64>,
503 #[clap(
504 long,
505 short = 'm',
506 help = "Max number of events to keep in TUI (0=unlimited)"
507 )]
508 pub max_events: Option<u64>,
509 #[clap(
510 long,
511 help = "Number of scrollback lines to keep in the pseudo terminal (1000 by default)",
512 conflicts_with = "no_tty"
513 )]
514 pub scrollback_lines: Option<usize>,
515 #[clap(
516 long = "theme",
517 help = "Path to a theme file to use for the TUI.",
518 value_parser = theme_file_cli_parser,
519 )]
520 pub theme_file: Option<ThemeFileValue>,
522 #[clap(skip)]
523 pub theme: Option<Box<crate::cli::tui_theme::ThemeSpec>>,
524 #[clap(skip)]
525 pub keys: Option<Box<TuiKeyBindingsConfig>>,
526}
527
528#[derive(Debug, Clone, PartialEq, Eq)]
529pub enum ThemeFileValue {
530 Cli(PathBuf),
531 Config(PathBuf),
532}
533
534impl ThemeFileValue {
535 pub fn as_deref(&self) -> &Path {
536 match self {
537 Self::Cli(path) | Self::Config(path) => path.as_path(),
538 }
539 }
540
541 pub fn is_from_cli(&self) -> bool {
542 matches!(self, Self::Cli(_))
543 }
544}
545
546impl fmt::Display for ThemeFileValue {
547 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
548 match self {
549 Self::Cli(path) | Self::Config(path) => write!(f, "{}", path.display()),
550 }
551 }
552}
553
554#[derive(Args, Debug, Default, Clone)]
555pub struct DebuggerArgs {
556 #[clap(
557 long,
558 short = 'D',
559 help = "Set the default external command to run when using \"Detach, Stop and Run Command\" feature in Hit Manager"
560 )]
561 pub default_external_command: Option<String>,
562 #[clap(
563 long = "add-breakpoint",
564 short = 'b',
565 value_parser = breakpoint_parser,
566 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",
567 )]
568 pub breakpoints: Vec<BreakPoint>,
569}
570
571impl TuiModeArgs {
572 pub const fn tty(&self) -> bool {
573 !self.no_tty
574 }
575
576 pub fn validate_pty_options(&self, pty_allocated: bool) -> color_eyre::Result<()> {
585 if pty_allocated {
586 return Ok(());
587 }
588 if self.active_pane == Some(ActivePane::Terminal) {
589 bail!(
590 "--active-pane terminal requires a pseudo terminal, which is not allocated in this mode"
591 );
592 }
593 if self.scrollback_lines.is_some() {
594 bail!("--scrollback-lines requires a pseudo terminal, which is not allocated in this mode");
595 }
596 Ok(())
597 }
598
599 pub fn merge_config(&mut self, config: TuiModeConfig) {
600 self.active_pane = self.active_pane.or(config.active_pane);
601 self.layout = self.layout.or(config.layout);
602 self.frame_rate = self.frame_rate.or(config.frame_rate);
603 self.max_events = self.max_events.or(config.max_events);
604 self.scrollback_lines = self.scrollback_lines.or(config.scrollback_lines);
605 if self.theme_file.is_none()
606 && let Some(path) = config.theme_file
607 {
608 self.theme_file = Some(ThemeFileValue::Config(path));
609 }
610 if self.theme.is_none() {
611 self.theme = config.theme.map(Box::new);
612 }
613 self.follow |= config.follow.unwrap_or_default();
614 if self.keys.is_none() {
615 self.keys = config.keys.map(Box::new);
616 }
617 if (!self.terminate_on_exit) && (!self.kill_on_exit) {
618 match config.exit_handling {
619 Some(ExitHandling::Kill) => self.kill_on_exit = true,
620 Some(ExitHandling::Terminate) => self.terminate_on_exit = true,
621 _ => (),
622 }
623 }
624 }
625}
626
627fn theme_file_cli_parser(s: &str) -> Result<ThemeFileValue, String> {
628 if s.is_empty() {
629 Err("theme file path cannot be empty".to_string())
630 } else {
631 Ok(ThemeFileValue::Cli(PathBuf::from(s)))
632 }
633}
634
635impl DebuggerArgs {
636 pub fn merge_config(&mut self, config: DebuggerConfig) {
637 if self.default_external_command.is_none() {
638 self.default_external_command = config.default_external_command;
639 }
640 }
641}
642
643fn frame_rate_parser(s: &str) -> Result<f64, ParseFrameRateError> {
644 let v = s.parse::<f64>().with_context(|_| ParseFloatSnafu {
645 value: s.to_string(),
646 })?;
647 if v < 0.0 || v.is_nan() || v.is_infinite() {
648 Err(ParseFrameRateError::Invalid)
649 } else if v < 5.0 {
650 Err(ParseFrameRateError::TooLow)
651 } else {
652 Ok(v)
653 }
654}
655
656fn breakpoint_parser(s: &str) -> Result<BreakPoint, Cow<'static, str>> {
657 BreakPoint::try_from(s)
658}
659
660#[derive(Snafu, Debug)]
661enum ParseFrameRateError {
662 #[snafu(display("Failed to parse frame rate {value} as a floating point number"))]
663 ParseFloat {
664 source: ParseFloatError,
665 value: String,
666 },
667 #[snafu(display("Invalid frame rate"))]
668 Invalid,
669 #[snafu(display("Frame rate too low, must be at least 5.0"))]
670 TooLow,
671}
672
673#[derive(Args, Debug, Default, Clone)]
674pub struct ExporterArgs {
675 #[clap(short, long, help = "prettify the output if supported")]
676 pub pretty: bool,
677}
678
679#[cfg(test)]
680mod tests {
681 use clap::Parser;
682 use test_that::prelude::*;
683
684 use super::*;
685
686 #[derive(Parser, Debug)]
688 struct TestCli<T: Args + Clone + std::fmt::Debug> {
689 #[clap(flatten)]
690 args: T,
691 }
692
693 #[test]
696 fn test_ptrace_args_merge_config() {
697 let mut args = PtraceArgs {
698 seccomp_bpf: SeccompBpf::Auto,
699 polling_interval: None,
700 };
701
702 let cfg = PtraceConfig {
703 seccomp_bpf: Some(SeccompBpf::On),
704 };
705
706 args.merge_config(cfg);
707 assert_eq!(args.seccomp_bpf, SeccompBpf::On);
708 }
709
710 #[test]
711 fn test_ptrace_args_cli_parse() {
712 let cli = TestCli::<PtraceArgs>::parse_from(["test", "--polling-interval", "100"]);
713 assert_eq!(cli.args.polling_interval, Some(100));
714 }
715
716 #[test]
719 fn test_modifier_processed_defaults() {
720 let args = ModifierArgs::default().processed();
721 assert!(args.resolve_proc_self_exe);
722 assert!(args.hide_cloexec_fds);
723 assert!(!args.timestamp);
724 assert!(args.inline_timestamp_format.is_some());
725 }
726
727 #[test]
728 fn test_modifier_processed_fd_implies_stdio() {
729 let args = ModifierArgs {
730 fd_in_cmdline: true,
731 ..Default::default()
732 }
733 .processed();
734
735 assert!(args.stdio_in_cmdline);
736 }
737
738 #[test]
739 fn test_modifier_merge_config() {
740 let mut args = ModifierArgs::default();
741
742 let cfg = ModifierConfig {
743 successful_only: Some(true),
744 fd_in_cmdline: Some(true),
745 stdio_in_cmdline: None,
746 resolve_proc_self_exe: Some(false),
747 hide_cloexec_fds: Some(false),
748 timestamp: None,
749 seccomp_bpf: None,
750 collect_cgroup: None,
751 };
752
753 args.merge_config(cfg);
754
755 assert!(args.successful_only);
756 assert!(args.fd_in_cmdline);
757 assert!(!args.resolve_proc_self_exe);
758 assert!(!args.hide_cloexec_fds);
759 }
760
761 #[test]
762 fn test_modifier_args_cli_overrides_config_positive() {
763 let mut args = ModifierArgs {
764 resolve_proc_self_exe: true, ..Default::default()
766 };
767
768 let cfg = ModifierConfig {
769 resolve_proc_self_exe: Some(false),
770 ..Default::default()
771 };
772
773 args.merge_config(cfg);
774
775 assert!(args.resolve_proc_self_exe);
776 }
777
778 #[test]
779 fn test_modifier_args_cli_no_flag_blocks_config() {
780 let mut args = ModifierArgs {
781 no_hide_cloexec_fds: true, ..Default::default()
783 };
784
785 let cfg = ModifierConfig {
786 hide_cloexec_fds: Some(true),
787 ..Default::default()
788 };
789
790 args.merge_config(cfg);
791
792 assert!(!args.hide_cloexec_fds);
793 }
794
795 #[test]
796 fn test_modifier_cli_parse_conflicts() {
797 let cli = TestCli::<ModifierArgs>::parse_from(["test", "--no-timestamp"]);
798
799 let processed = cli.args.processed();
800 assert!(!processed.timestamp);
801 }
802
803 #[test]
804 fn test_modifier_args_timestamp_cli_overrides_config() {
805 let mut args = ModifierArgs {
806 timestamp: true,
807 ..Default::default()
808 };
809
810 let cfg = ModifierConfig {
811 timestamp: Some(crate::cli::config::TimestampConfig {
812 enable: false,
813 inline_format: None,
814 }),
815 ..Default::default()
816 };
817
818 args.merge_config(cfg);
819
820 assert!(args.timestamp);
821 }
822
823 #[test]
826 fn test_tracer_event_filter_parser_basic() {
827 let f = tracer_event_filter_parser("warning,error").unwrap();
828 assert!(f.contains(TracerEventDetailsKind::Warning));
829 assert!(f.contains(TracerEventDetailsKind::Error));
830 }
831
832 #[test]
833 fn test_tracer_event_filter_duplicate() {
834 let err = tracer_event_filter_parser("warning,warning").unwrap_err();
835 assert_that!(err, contains_substring("already included"));
836 }
837
838 #[test]
839 fn test_tracer_event_args_all() {
840 let args = TracerEventArgs::all();
841 let f = args.filter().unwrap();
842 assert_eq!(f, BitFlags::all());
843 }
844
845 #[test]
846 fn test_tracer_event_include_exclude_conflict() {
847 let args = TracerEventArgs {
848 show_all_events: false,
849 filter: BitFlags::empty(),
850 filter_include: TracerEventDetailsKind::Error.into(),
851 filter_exclude: TracerEventDetailsKind::Error.into(),
852 };
853
854 assert_that!(args.filter(), err(anything()));
855 }
856
857 #[test]
860 fn test_logmode_foreground_logic() {
861 let args = LogModeArgs {
862 foreground: false,
863 no_foreground: true,
864 ..Default::default()
865 };
866 assert!(!args.foreground());
867
868 let args = LogModeArgs {
869 foreground: true,
870 no_foreground: false,
871 ..Default::default()
872 };
873 assert!(args.foreground());
874 }
875
876 #[test]
877 fn test_logmode_merge_color_config() {
878 let mut args = LogModeArgs::default();
879
880 let cfg = LogModeConfig {
881 color_level: Some(ColorLevel::Less),
882 ..Default::default()
883 };
884
885 args.merge_config(cfg);
886 assert!(args.less_colors);
887 }
888
889 #[test]
890 fn test_logmode_fd_display_config() {
891 let mut args = LogModeArgs::default();
892
893 let cfg = LogModeConfig {
894 fd_display: Some(FileDescriptorDisplay::Show),
895 ..Default::default()
896 };
897
898 args.merge_config(cfg);
899 assert!(args.show_fd);
900 }
901
902 #[test]
903 fn test_logmode_cli_parse() {
904 let cli = TestCli::<LogModeArgs>::parse_from(["test", "--show-cmdline", "--show-interpreter"]);
905
906 assert!(cli.args.show_cmdline);
907 assert!(cli.args.show_interpreter);
908 }
909
910 #[test]
911 fn test_logmode_cli_no_foreground_overrides_config() {
912 let mut args = LogModeArgs {
913 no_foreground: true,
914 ..Default::default()
915 };
916
917 let cfg = LogModeConfig {
918 foreground: Some(true),
919 ..Default::default()
920 };
921
922 args.merge_config(cfg);
923
924 assert!(!args.foreground());
925 }
926
927 #[test]
928 fn test_logmode_cli_show_fd_overrides_config_hide() {
929 let mut args = LogModeArgs {
930 show_fd: true,
931 ..Default::default()
932 };
933
934 let cfg = LogModeConfig {
935 fd_display: Some(FileDescriptorDisplay::Hide),
936 ..Default::default()
937 };
938
939 args.merge_config(cfg);
940
941 assert!(args.show_fd);
942 assert!(!args.no_show_fd);
943 }
944
945 #[test]
946 fn test_logmode_cli_color_overrides_config() {
947 let mut args = LogModeArgs {
948 more_colors: true,
949 ..Default::default()
950 };
951
952 let cfg = LogModeConfig {
953 color_level: Some(ColorLevel::Less),
954 ..Default::default()
955 };
956
957 args.merge_config(cfg);
958
959 assert!(args.more_colors);
960 assert!(!args.less_colors);
961 }
962
963 #[test]
966 fn test_tui_merge_config_exit_handling() {
967 let mut args = TuiModeArgs::default();
968
969 let cfg = TuiModeConfig {
970 exit_handling: Some(ExitHandling::Kill),
971 follow: Some(true),
972 theme_file: Some(PathBuf::from("high-contrast.toml")),
973 ..Default::default()
974 };
975
976 args.merge_config(cfg);
977
978 assert!(args.kill_on_exit);
979 assert!(args.follow);
980 assert_eq!(
981 args.theme_file,
982 Some(ThemeFileValue::Config(PathBuf::from("high-contrast.toml")))
983 );
984 }
985
986 #[test]
987 fn test_tui_merge_config_theme_file_from_cli() {
988 let mut args = TuiModeArgs {
989 theme_file: Some(ThemeFileValue::Cli(PathBuf::from("cli.toml"))),
990 ..Default::default()
991 };
992 let cfg = TuiModeConfig {
993 theme_file: Some(PathBuf::from("config.toml")),
994 ..Default::default()
995 };
996
997 args.merge_config(cfg);
998
999 assert_eq!(
1001 args.theme_file,
1002 Some(ThemeFileValue::Cli(PathBuf::from("cli.toml")))
1003 );
1004 }
1005
1006 #[test]
1007 fn test_tui_parse_theme_file_from_cli() {
1008 let args = TestCli::<TuiModeArgs>::parse_from(["test", "--theme", "cli.toml"]).args;
1009 assert_eq!(
1010 args.theme_file,
1011 Some(ThemeFileValue::Cli(PathBuf::from("cli.toml")))
1012 );
1013 }
1014
1015 #[test]
1016 fn test_tui_parse_theme_file_unset_by_default() {
1017 let args = TestCli::<TuiModeArgs>::parse_from(["test"]).args;
1018 assert_eq!(args.theme_file, None);
1019 }
1020
1021 #[test]
1022 fn test_tui_merge_config_inline_theme() {
1023 use crate::cli::tui_theme::{
1024 StyleSpec,
1025 ThemeColor,
1026 ThemeSpec,
1027 };
1028 let mut args = TuiModeArgs::default();
1029 let cfg = TuiModeConfig {
1030 theme: Some(ThemeSpec {
1031 app_title: Some(StyleSpec {
1032 fg: Some(ThemeColor::Named("cyan".into())),
1033 ..Default::default()
1034 }),
1035 ..Default::default()
1036 }),
1037 ..Default::default()
1038 };
1039
1040 args.merge_config(cfg);
1041
1042 assert!(matches!(
1043 args
1044 .theme
1045 .as_ref()
1046 .and_then(|s| s.app_title.as_ref())
1047 .and_then(|a| a.fg.as_ref()),
1048 Some(ThemeColor::Named(s)) if s == "cyan"
1049 ));
1050 }
1051
1052 #[test]
1053 fn test_tui_validate_pty_options() {
1054 let args = TuiModeArgs {
1056 active_pane: Some(crate::cli::options::ActivePane::Terminal),
1057 layout: Some(crate::cli::options::AppLayout::Vertical),
1058 scrollback_lines: Some(2000),
1059 ..Default::default()
1060 };
1061 assert!(args.validate_pty_options(true).is_ok());
1062
1063 assert_that!(args.validate_pty_options(false), err(anything()));
1065 assert_that!(
1066 TuiModeArgs {
1067 scrollback_lines: Some(1000),
1068 ..Default::default()
1069 }
1070 .validate_pty_options(false),
1071 err(anything())
1072 );
1073 assert!(
1076 TuiModeArgs {
1077 active_pane: Some(crate::cli::options::ActivePane::Events),
1078 ..Default::default()
1079 }
1080 .validate_pty_options(false)
1081 .is_ok()
1082 );
1083 assert!(
1084 TuiModeArgs {
1085 layout: Some(crate::cli::options::AppLayout::Vertical),
1086 ..Default::default()
1087 }
1088 .validate_pty_options(false)
1089 .is_ok()
1090 );
1091 assert!(TuiModeArgs::default().validate_pty_options(false).is_ok());
1092 }
1093
1094 #[test]
1095 fn test_tui_cli_parse() {
1096 let cli = TestCli::<TuiModeArgs>::parse_from(["test", "--follow", "--frame-rate", "30"]);
1097
1098 assert!(cli.args.tty());
1099 assert!(!cli.args.no_tty);
1100 assert!(cli.args.follow);
1101 assert_eq!(cli.args.frame_rate, Some(30.0));
1102 }
1103
1104 #[test]
1105 fn test_tui_cli_parse_no_tty() {
1106 let cli = TestCli::<TuiModeArgs>::parse_from(["test", "--no-tty"]);
1107
1108 assert!(!cli.args.tty());
1109 assert!(cli.args.no_tty);
1110 }
1111
1112 #[test]
1113 fn test_tui_cli_no_tty_conflicts_with_terminal_options() {
1114 let result =
1115 TestCli::<TuiModeArgs>::try_parse_from(["test", "--no-tty", "--active-pane", "terminal"]);
1116
1117 assert_that!(result, err(anything()));
1118 }
1119
1120 #[test]
1121 fn test_tui_cli_exit_handling_overrides_config() {
1122 let mut args = TuiModeArgs {
1123 terminate_on_exit: true,
1124 ..Default::default()
1125 };
1126
1127 let cfg = TuiModeConfig {
1128 exit_handling: Some(ExitHandling::Kill),
1129 ..Default::default()
1130 };
1131
1132 args.merge_config(cfg);
1133
1134 assert!(args.terminate_on_exit);
1135 assert!(!args.kill_on_exit);
1136 }
1137
1138 #[test]
1141 fn test_debugger_merge_config() {
1142 let mut args = DebuggerArgs::default();
1143
1144 let cfg = DebuggerConfig {
1145 default_external_command: Some("echo hi".into()),
1146 };
1147
1148 args.merge_config(cfg);
1149 assert_eq!(args.default_external_command.as_deref(), Some("echo hi"));
1150 }
1151
1152 #[test]
1153 fn test_debugger_cli_parse_breakpoint() {
1154 let cli = TestCli::<DebuggerArgs>::parse_from([
1155 "test",
1156 "--add-breakpoint",
1157 "sysenter:exact-filename:/bin/ls",
1158 ]);
1159
1160 assert_eq!(cli.args.breakpoints.len(), 1);
1161 }
1162
1163 #[test]
1164 fn test_debugger_cli_command_overrides_config() {
1165 let mut args = DebuggerArgs {
1166 default_external_command: Some("cli-cmd".into()),
1167 ..Default::default()
1168 };
1169
1170 let cfg = DebuggerConfig {
1171 default_external_command: Some("config-cmd".into()),
1172 };
1173
1174 args.merge_config(cfg);
1175
1176 assert_eq!(args.default_external_command.as_deref(), Some("cli-cmd"));
1177 }
1178
1179 #[test]
1182 fn test_frame_rate_parser_valid() {
1183 assert_eq!(frame_rate_parser("60").unwrap(), 60.0);
1184 }
1185
1186 #[test]
1187 fn test_frame_rate_parser_too_low() {
1188 let err = frame_rate_parser("1").unwrap_err();
1189 let msg = err.to_string();
1190 assert_that!(msg, contains_substring("too low"));
1191 }
1192
1193 #[test]
1194 fn test_frame_rate_parser_invalid() {
1195 let err = frame_rate_parser("-1").unwrap_err();
1196 assert_that!(err.to_string(), contains_substring("Invalid"));
1197 }
1198
1199 #[test]
1202 fn test_exporter_cli_parse() {
1203 let cli = TestCli::<ExporterArgs>::parse_from(["test", "--pretty"]);
1204
1205 assert!(cli.args.pretty);
1206 }
1207}