use std::{
borrow::Cow,
fmt,
num::ParseFloatError,
path::{
Path,
PathBuf,
},
};
use clap::{
Args,
ValueEnum,
};
use color_eyre::eyre::bail;
use enumflags2::BitFlags;
use snafu::{
ResultExt,
Snafu,
};
use super::{
config::{
DebuggerConfig,
ExitHandling,
LogModeConfig,
ModifierConfig,
PtraceConfig,
TuiModeConfig,
},
keys::TuiKeyBindingsConfig,
options::{
ActivePane,
AppLayout,
SeccompBpf,
},
};
use crate::{
breakpoint::BreakPoint,
cli::config::{
ColorLevel,
EnvDisplay,
FileDescriptorDisplay,
},
event::TracerEventDetailsKind,
timestamp::TimestampFormat,
};
#[derive(Args, Debug, Default, Clone)]
pub struct PtraceArgs {
#[clap(long, help = "Controls whether to enable seccomp-bpf optimization, which greatly improves performance", default_value_t = SeccompBpf::Auto)]
pub seccomp_bpf: SeccompBpf,
#[clap(
long,
help = "Polling interval, in microseconds. -1(default) disables polling."
)]
pub polling_interval: Option<i64>,
}
#[derive(Args, Debug, Default, Clone)]
pub struct ModifierArgs {
#[clap(long, help = "Only show successful calls", default_value_t = false)]
pub successful_only: bool,
#[clap(
long,
help = "[Experimental] Try to reproduce file descriptors in commandline. This might result in an unexecutable cmdline if pipes, sockets, etc. are involved.",
default_value_t = false
)]
pub fd_in_cmdline: bool,
#[clap(
long,
help = "[Experimental] Try to reproduce stdio in commandline. This might result in an unexecutable cmdline if pipes, sockets, etc. are involved.",
default_value_t = false
)]
pub stdio_in_cmdline: bool,
#[clap(long, help = "Resolve /proc/self/exe symlink", default_value_t = false)]
pub resolve_proc_self_exe: bool,
#[clap(
long,
help = "Do not resolve /proc/self/exe symlink",
default_value_t = false,
conflicts_with = "resolve_proc_self_exe"
)]
pub no_resolve_proc_self_exe: bool,
#[clap(long, help = "Hide CLOEXEC fds", default_value_t = false)]
pub hide_cloexec_fds: bool,
#[clap(
long,
help = "Do not hide CLOEXEC fds",
default_value_t = false,
conflicts_with = "hide_cloexec_fds"
)]
pub no_hide_cloexec_fds: bool,
#[clap(long, help = "Show timestamp information", default_value_t = false)]
pub timestamp: bool,
#[clap(
long,
help = "Do not show timestamp information",
default_value_t = false,
conflicts_with = "timestamp"
)]
pub no_timestamp: bool,
#[clap(
long,
help = "Set the format of inline timestamp. See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for available options."
)]
pub inline_timestamp_format: Option<TimestampFormat>,
#[clap(long, help = "Collect cgroup information", default_value_t = false)]
pub collect_cgroup: bool,
#[clap(
long,
help = "Do not collect cgroup information",
default_value_t = false,
conflicts_with = "collect_cgroup"
)]
pub no_collect_cgroup: bool,
}
impl PtraceArgs {
pub fn merge_config(&mut self, config: PtraceConfig) {
if let Some(setting) = config.seccomp_bpf
&& self.seccomp_bpf == SeccompBpf::Auto
{
self.seccomp_bpf = setting;
}
}
}
impl ModifierArgs {
pub fn processed(mut self) -> Self {
self.stdio_in_cmdline = self.fd_in_cmdline || self.stdio_in_cmdline;
self.resolve_proc_self_exe = match (self.resolve_proc_self_exe, self.no_resolve_proc_self_exe) {
(true, false) => true,
(false, true) => false,
_ => true, };
self.hide_cloexec_fds = match (self.hide_cloexec_fds, self.no_hide_cloexec_fds) {
(true, false) => true,
(false, true) => false,
_ => true, };
self.timestamp = match (self.timestamp, self.no_timestamp) {
(true, false) => true,
(false, true) => false,
_ => false, };
self.collect_cgroup = match (self.collect_cgroup, self.no_collect_cgroup) {
(true, false) => true,
(false, true) => false,
_ => false, };
self
.inline_timestamp_format
.get_or_insert_with(TimestampFormat::default);
self
}
pub fn merge_config(&mut self, config: ModifierConfig) {
self.successful_only = self.successful_only || config.successful_only.unwrap_or_default();
self.fd_in_cmdline |= config.fd_in_cmdline.unwrap_or_default();
self.stdio_in_cmdline |= config.stdio_in_cmdline.unwrap_or_default();
if (!self.no_resolve_proc_self_exe) && (!self.resolve_proc_self_exe) {
self.resolve_proc_self_exe = config.resolve_proc_self_exe.unwrap_or_default();
}
if (!self.no_hide_cloexec_fds) && (!self.hide_cloexec_fds) {
self.hide_cloexec_fds = config.hide_cloexec_fds.unwrap_or_default();
}
if let Some(c) = config.timestamp {
if (!self.timestamp) && (!self.no_timestamp) {
self.timestamp = c.enable;
}
if self.inline_timestamp_format.is_none() {
self.inline_timestamp_format = c.inline_format;
}
}
if (!self.no_collect_cgroup) && (!self.collect_cgroup) {
self.collect_cgroup = config.collect_cgroup.unwrap_or_default();
}
}
}
#[derive(Args, Debug)]
pub struct TracerEventArgs {
#[clap(
long,
help = "Set the default filter to show all events. This option can be used in combination with --filter-exclude to exclude some unwanted events.",
conflicts_with = "filter"
)]
pub show_all_events: bool,
#[clap(
long,
help = "Set the default filter for events.",
value_parser = tracer_event_filter_parser,
default_value = "warning,error,exec,tracee-exit"
)]
pub filter: BitFlags<TracerEventDetailsKind>,
#[clap(
long,
help = "Aside from the default filter, also include the events specified here.",
required = false,
value_parser = tracer_event_filter_parser,
default_value_t = BitFlags::empty()
)]
pub filter_include: BitFlags<TracerEventDetailsKind>,
#[clap(
long,
help = "Exclude the events specified here from the default filter.",
value_parser = tracer_event_filter_parser,
default_value_t = BitFlags::empty()
)]
pub filter_exclude: BitFlags<TracerEventDetailsKind>,
}
fn tracer_event_filter_parser(filter: &str) -> Result<BitFlags<TracerEventDetailsKind>, String> {
let mut result = BitFlags::empty();
if filter == "<empty>" {
return Ok(result);
}
for f in filter.split(',') {
let kind = TracerEventDetailsKind::from_str(f, false)?;
if result.contains(kind) {
return Err(format!(
"Event kind '{kind}' is already included in the filter"
));
}
result |= kind;
}
Ok(result)
}
impl TracerEventArgs {
pub fn all() -> Self {
Self {
show_all_events: true,
filter: Default::default(),
filter_include: Default::default(),
filter_exclude: Default::default(),
}
}
pub fn filter(&self) -> color_eyre::Result<BitFlags<TracerEventDetailsKind>> {
let default_filter = if self.show_all_events {
BitFlags::all()
} else {
self.filter
};
if self.filter_include.intersects(self.filter_exclude) {
bail!("filter_include and filter_exclude cannot contain common events");
}
let mut filter = default_filter | self.filter_include;
filter.remove(self.filter_exclude);
Ok(filter)
}
}
#[derive(Args, Debug, Default, Clone)]
pub struct LogModeArgs {
#[clap(long, help = "More colors", conflicts_with = "less_colors")]
pub more_colors: bool,
#[clap(long, help = "Less colors", conflicts_with = "more_colors")]
pub less_colors: bool,
#[clap(
long,
help = "Print commandline that (hopefully) reproduces what was executed. Note: file descriptors are not handled for now.",
conflicts_with_all = ["show_env", "diff_env", "show_argv", "no_show_cmdline"]
)]
pub show_cmdline: bool,
#[clap(
long,
help = "Don't print commandline that (hopefully) reproduces what was executed."
)]
pub no_show_cmdline: bool,
#[clap(
long,
help = "Try to show script interpreter indicated by shebang",
conflicts_with = "no_show_interpreter"
)]
pub show_interpreter: bool,
#[clap(
long,
help = "Do not show script interpreter indicated by shebang",
conflicts_with = "show_interpreter"
)]
pub no_show_interpreter: bool,
#[clap(
long,
help = "Set the terminal foreground process group to tracee. This option is useful when tracexec is used interactively. [default]",
conflicts_with = "no_foreground"
)]
pub foreground: bool,
#[clap(
long,
help = "Do not set the terminal foreground process group to tracee",
conflicts_with = "foreground"
)]
pub no_foreground: bool,
#[clap(
long,
help = "Diff file descriptors with the original std{in/out/err}",
conflicts_with = "no_diff_fd"
)]
pub diff_fd: bool,
#[clap(
long,
help = "Do not diff file descriptors",
conflicts_with = "diff_fd"
)]
pub no_diff_fd: bool,
#[clap(long, help = "Show file descriptors", conflicts_with = "diff_fd")]
pub show_fd: bool,
#[clap(
long,
help = "Do not show file descriptors",
conflicts_with = "show_fd"
)]
pub no_show_fd: bool,
#[clap(
long,
help = "Diff environment variables with the original environment",
conflicts_with = "no_diff_env",
conflicts_with = "show_env",
conflicts_with = "no_show_env"
)]
pub diff_env: bool,
#[clap(
long,
help = "Do not diff environment variables",
conflicts_with = "diff_env"
)]
pub no_diff_env: bool,
#[clap(
long,
help = "Show environment variables",
conflicts_with = "no_show_env",
conflicts_with = "diff_env"
)]
pub show_env: bool,
#[clap(
long,
help = "Do not show environment variables",
conflicts_with = "show_env"
)]
pub no_show_env: bool,
#[clap(long, help = "Show comm", conflicts_with = "no_show_comm")]
pub show_comm: bool,
#[clap(long, help = "Do not show comm", conflicts_with = "show_comm")]
pub no_show_comm: bool,
#[clap(long, help = "Show argv", conflicts_with = "no_show_argv")]
pub show_argv: bool,
#[clap(long, help = "Do not show argv", conflicts_with = "show_argv")]
pub no_show_argv: bool,
#[clap(long, help = "Show filename", conflicts_with = "no_show_filename")]
pub show_filename: bool,
#[clap(long, help = "Do not show filename", conflicts_with = "show_filename")]
pub no_show_filename: bool,
#[clap(long, help = "Show cwd", conflicts_with = "no_show_cwd")]
pub show_cwd: bool,
#[clap(long, help = "Do not show cwd", conflicts_with = "show_cwd")]
pub no_show_cwd: bool,
#[clap(long, help = "Decode errno values", conflicts_with = "no_decode_errno")]
pub decode_errno: bool,
#[clap(
long,
help = "Do not decode errno values",
conflicts_with = "decode_errno"
)]
pub no_decode_errno: bool,
}
impl LogModeArgs {
pub fn foreground(&self) -> bool {
match (self.foreground, self.no_foreground) {
(false, true) => false,
(true, false) => true,
_ => true,
}
}
pub fn merge_config(&mut self, config: LogModeConfig) {
macro_rules! fallback {
($x:ident) => {
::paste::paste! {
if (!self.$x) && (!self.[<no_ $x>]) {
if let Some(x) = config.$x {
if x {
self.$x = true;
} else {
self.[<no_ $x>] = true;
}
}
}
}
};
}
fallback!(show_interpreter);
fallback!(foreground);
fallback!(show_comm);
fallback!(show_filename);
fallback!(show_cwd);
fallback!(decode_errno);
match config.fd_display {
Some(FileDescriptorDisplay::Show) => {
if (!self.no_show_fd) && (!self.diff_fd) {
self.show_fd = true;
}
}
Some(FileDescriptorDisplay::Diff) => {
if (!self.show_fd) && (!self.no_diff_fd) {
self.diff_fd = true;
}
}
Some(FileDescriptorDisplay::Hide) if (!self.diff_fd) && (!self.show_fd) => {
self.no_diff_fd = true;
self.no_show_fd = true;
}
_ => (),
}
fallback!(show_cmdline);
if !self.show_cmdline {
fallback!(show_argv);
tracing::warn!("{}", self.show_argv);
match config.env_display {
Some(EnvDisplay::Show) => {
if (!self.diff_env) && (!self.no_show_env) {
self.show_env = true;
}
}
Some(EnvDisplay::Diff) => {
if (!self.show_env) && (!self.no_diff_env) {
self.diff_env = true;
}
}
Some(EnvDisplay::Hide) if (!self.show_env) && (!self.diff_env) => {
self.no_diff_env = true;
self.no_show_env = true;
}
_ => (),
}
}
match config.color_level {
Some(ColorLevel::Less) => {
if !self.more_colors {
self.less_colors = true;
}
}
Some(ColorLevel::More) if !self.less_colors => {
self.more_colors = true;
}
_ => (),
}
}
}
#[derive(Args, Debug, Default, Clone)]
pub struct TuiModeArgs {
#[clap(
long,
help = "Do not allocate a pseudo terminal; redirect stdin/out/err to /dev/null"
)]
pub no_tty: bool,
#[clap(long, short, help = "Keep the event list scrolled to the bottom")]
pub follow: bool,
#[clap(
long,
help = "Instead of waiting for the root child to exit, terminate when the TUI exits",
conflicts_with = "kill_on_exit"
)]
pub terminate_on_exit: bool,
#[clap(
long,
help = "Instead of waiting for the root child to exit, kill when the TUI exits"
)]
pub kill_on_exit: bool,
#[clap(
long,
short = 'A',
help = "Set the default active pane to use when TUI launches",
conflicts_with = "no_tty"
)]
pub active_pane: Option<ActivePane>,
#[clap(
long,
short = 'L',
help = "Set the layout of the TUI when it launches",
conflicts_with = "no_tty"
)]
pub layout: Option<AppLayout>,
#[clap(
long,
short = 'F',
help = "Set the frame rate of the TUI (60 by default)",
value_parser = frame_rate_parser
)]
pub frame_rate: Option<f64>,
#[clap(
long,
short = 'm',
help = "Max number of events to keep in TUI (0=unlimited)"
)]
pub max_events: Option<u64>,
#[clap(
long,
help = "Number of scrollback lines to keep in the pseudo terminal (1000 by default)",
conflicts_with = "no_tty"
)]
pub scrollback_lines: Option<usize>,
#[clap(
long = "theme",
help = "Path to a theme file to use for the TUI.",
value_parser = theme_file_cli_parser,
)]
pub theme_file: Option<ThemeFileValue>,
#[clap(skip)]
pub theme: Option<Box<crate::cli::tui_theme::ThemeSpec>>,
#[clap(skip)]
pub keys: Option<Box<TuiKeyBindingsConfig>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ThemeFileValue {
Cli(PathBuf),
Config(PathBuf),
}
impl ThemeFileValue {
pub fn as_deref(&self) -> &Path {
match self {
Self::Cli(path) | Self::Config(path) => path.as_path(),
}
}
pub fn is_from_cli(&self) -> bool {
matches!(self, Self::Cli(_))
}
}
impl fmt::Display for ThemeFileValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cli(path) | Self::Config(path) => write!(f, "{}", path.display()),
}
}
}
#[derive(Args, Debug, Default, Clone)]
pub struct DebuggerArgs {
#[clap(
long,
short = 'D',
help = "Set the default external command to run when using \"Detach, Stop and Run Command\" feature in Hit Manager"
)]
pub default_external_command: Option<String>,
#[clap(
long = "add-breakpoint",
short = 'b',
value_parser = breakpoint_parser,
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",
)]
pub breakpoints: Vec<BreakPoint>,
}
impl TuiModeArgs {
pub const fn tty(&self) -> bool {
!self.no_tty
}
pub fn validate_pty_options(&self, pty_allocated: bool) -> color_eyre::Result<()> {
if pty_allocated {
return Ok(());
}
if self.active_pane == Some(ActivePane::Terminal) {
bail!(
"--active-pane terminal requires a pseudo terminal, which is not allocated in this mode"
);
}
if self.scrollback_lines.is_some() {
bail!("--scrollback-lines requires a pseudo terminal, which is not allocated in this mode");
}
Ok(())
}
pub fn merge_config(&mut self, config: TuiModeConfig) {
self.active_pane = self.active_pane.or(config.active_pane);
self.layout = self.layout.or(config.layout);
self.frame_rate = self.frame_rate.or(config.frame_rate);
self.max_events = self.max_events.or(config.max_events);
self.scrollback_lines = self.scrollback_lines.or(config.scrollback_lines);
if self.theme_file.is_none()
&& let Some(path) = config.theme_file
{
self.theme_file = Some(ThemeFileValue::Config(path));
}
if self.theme.is_none() {
self.theme = config.theme.map(Box::new);
}
self.follow |= config.follow.unwrap_or_default();
if self.keys.is_none() {
self.keys = config.keys.map(Box::new);
}
if (!self.terminate_on_exit) && (!self.kill_on_exit) {
match config.exit_handling {
Some(ExitHandling::Kill) => self.kill_on_exit = true,
Some(ExitHandling::Terminate) => self.terminate_on_exit = true,
_ => (),
}
}
}
}
fn theme_file_cli_parser(s: &str) -> Result<ThemeFileValue, String> {
if s.is_empty() {
Err("theme file path cannot be empty".to_string())
} else {
Ok(ThemeFileValue::Cli(PathBuf::from(s)))
}
}
impl DebuggerArgs {
pub fn merge_config(&mut self, config: DebuggerConfig) {
if self.default_external_command.is_none() {
self.default_external_command = config.default_external_command;
}
}
}
fn frame_rate_parser(s: &str) -> Result<f64, ParseFrameRateError> {
let v = s.parse::<f64>().with_context(|_| ParseFloatSnafu {
value: s.to_string(),
})?;
if v < 0.0 || v.is_nan() || v.is_infinite() {
Err(ParseFrameRateError::Invalid)
} else if v < 5.0 {
Err(ParseFrameRateError::TooLow)
} else {
Ok(v)
}
}
fn breakpoint_parser(s: &str) -> Result<BreakPoint, Cow<'static, str>> {
BreakPoint::try_from(s)
}
#[derive(Snafu, Debug)]
enum ParseFrameRateError {
#[snafu(display("Failed to parse frame rate {value} as a floating point number"))]
ParseFloat {
source: ParseFloatError,
value: String,
},
#[snafu(display("Invalid frame rate"))]
Invalid,
#[snafu(display("Frame rate too low, must be at least 5.0"))]
TooLow,
}
#[derive(Args, Debug, Default, Clone)]
pub struct ExporterArgs {
#[clap(short, long, help = "prettify the output if supported")]
pub pretty: bool,
}
#[cfg(test)]
mod tests {
use clap::Parser;
use test_that::prelude::*;
use super::*;
#[derive(Parser, Debug)]
struct TestCli<T: Args + Clone + std::fmt::Debug> {
#[clap(flatten)]
args: T,
}
#[test]
fn test_ptrace_args_merge_config() {
let mut args = PtraceArgs {
seccomp_bpf: SeccompBpf::Auto,
polling_interval: None,
};
let cfg = PtraceConfig {
seccomp_bpf: Some(SeccompBpf::On),
};
args.merge_config(cfg);
assert_eq!(args.seccomp_bpf, SeccompBpf::On);
}
#[test]
fn test_ptrace_args_cli_parse() {
let cli = TestCli::<PtraceArgs>::parse_from(["test", "--polling-interval", "100"]);
assert_eq!(cli.args.polling_interval, Some(100));
}
#[test]
fn test_modifier_processed_defaults() {
let args = ModifierArgs::default().processed();
assert!(args.resolve_proc_self_exe);
assert!(args.hide_cloexec_fds);
assert!(!args.timestamp);
assert!(args.inline_timestamp_format.is_some());
}
#[test]
fn test_modifier_processed_fd_implies_stdio() {
let args = ModifierArgs {
fd_in_cmdline: true,
..Default::default()
}
.processed();
assert!(args.stdio_in_cmdline);
}
#[test]
fn test_modifier_merge_config() {
let mut args = ModifierArgs::default();
let cfg = ModifierConfig {
successful_only: Some(true),
fd_in_cmdline: Some(true),
stdio_in_cmdline: None,
resolve_proc_self_exe: Some(false),
hide_cloexec_fds: Some(false),
timestamp: None,
seccomp_bpf: None,
collect_cgroup: None,
};
args.merge_config(cfg);
assert!(args.successful_only);
assert!(args.fd_in_cmdline);
assert!(!args.resolve_proc_self_exe);
assert!(!args.hide_cloexec_fds);
}
#[test]
fn test_modifier_args_cli_overrides_config_positive() {
let mut args = ModifierArgs {
resolve_proc_self_exe: true, ..Default::default()
};
let cfg = ModifierConfig {
resolve_proc_self_exe: Some(false),
..Default::default()
};
args.merge_config(cfg);
assert!(args.resolve_proc_self_exe);
}
#[test]
fn test_modifier_args_cli_no_flag_blocks_config() {
let mut args = ModifierArgs {
no_hide_cloexec_fds: true, ..Default::default()
};
let cfg = ModifierConfig {
hide_cloexec_fds: Some(true),
..Default::default()
};
args.merge_config(cfg);
assert!(!args.hide_cloexec_fds);
}
#[test]
fn test_modifier_cli_parse_conflicts() {
let cli = TestCli::<ModifierArgs>::parse_from(["test", "--no-timestamp"]);
let processed = cli.args.processed();
assert!(!processed.timestamp);
}
#[test]
fn test_modifier_args_timestamp_cli_overrides_config() {
let mut args = ModifierArgs {
timestamp: true,
..Default::default()
};
let cfg = ModifierConfig {
timestamp: Some(crate::cli::config::TimestampConfig {
enable: false,
inline_format: None,
}),
..Default::default()
};
args.merge_config(cfg);
assert!(args.timestamp);
}
#[test]
fn test_tracer_event_filter_parser_basic() {
let f = tracer_event_filter_parser("warning,error").unwrap();
assert!(f.contains(TracerEventDetailsKind::Warning));
assert!(f.contains(TracerEventDetailsKind::Error));
}
#[test]
fn test_tracer_event_filter_duplicate() {
let err = tracer_event_filter_parser("warning,warning").unwrap_err();
assert_that!(err, contains_substring("already included"));
}
#[test]
fn test_tracer_event_args_all() {
let args = TracerEventArgs::all();
let f = args.filter().unwrap();
assert_eq!(f, BitFlags::all());
}
#[test]
fn test_tracer_event_include_exclude_conflict() {
let args = TracerEventArgs {
show_all_events: false,
filter: BitFlags::empty(),
filter_include: TracerEventDetailsKind::Error.into(),
filter_exclude: TracerEventDetailsKind::Error.into(),
};
assert_that!(args.filter(), err(anything()));
}
#[test]
fn test_logmode_foreground_logic() {
let args = LogModeArgs {
foreground: false,
no_foreground: true,
..Default::default()
};
assert!(!args.foreground());
let args = LogModeArgs {
foreground: true,
no_foreground: false,
..Default::default()
};
assert!(args.foreground());
}
#[test]
fn test_logmode_merge_color_config() {
let mut args = LogModeArgs::default();
let cfg = LogModeConfig {
color_level: Some(ColorLevel::Less),
..Default::default()
};
args.merge_config(cfg);
assert!(args.less_colors);
}
#[test]
fn test_logmode_fd_display_config() {
let mut args = LogModeArgs::default();
let cfg = LogModeConfig {
fd_display: Some(FileDescriptorDisplay::Show),
..Default::default()
};
args.merge_config(cfg);
assert!(args.show_fd);
}
#[test]
fn test_logmode_cli_parse() {
let cli = TestCli::<LogModeArgs>::parse_from(["test", "--show-cmdline", "--show-interpreter"]);
assert!(cli.args.show_cmdline);
assert!(cli.args.show_interpreter);
}
#[test]
fn test_logmode_cli_no_foreground_overrides_config() {
let mut args = LogModeArgs {
no_foreground: true,
..Default::default()
};
let cfg = LogModeConfig {
foreground: Some(true),
..Default::default()
};
args.merge_config(cfg);
assert!(!args.foreground());
}
#[test]
fn test_logmode_cli_show_fd_overrides_config_hide() {
let mut args = LogModeArgs {
show_fd: true,
..Default::default()
};
let cfg = LogModeConfig {
fd_display: Some(FileDescriptorDisplay::Hide),
..Default::default()
};
args.merge_config(cfg);
assert!(args.show_fd);
assert!(!args.no_show_fd);
}
#[test]
fn test_logmode_cli_color_overrides_config() {
let mut args = LogModeArgs {
more_colors: true,
..Default::default()
};
let cfg = LogModeConfig {
color_level: Some(ColorLevel::Less),
..Default::default()
};
args.merge_config(cfg);
assert!(args.more_colors);
assert!(!args.less_colors);
}
#[test]
fn test_tui_merge_config_exit_handling() {
let mut args = TuiModeArgs::default();
let cfg = TuiModeConfig {
exit_handling: Some(ExitHandling::Kill),
follow: Some(true),
theme_file: Some(PathBuf::from("high-contrast.toml")),
..Default::default()
};
args.merge_config(cfg);
assert!(args.kill_on_exit);
assert!(args.follow);
assert_eq!(
args.theme_file,
Some(ThemeFileValue::Config(PathBuf::from("high-contrast.toml")))
);
}
#[test]
fn test_tui_merge_config_theme_file_from_cli() {
let mut args = TuiModeArgs {
theme_file: Some(ThemeFileValue::Cli(PathBuf::from("cli.toml"))),
..Default::default()
};
let cfg = TuiModeConfig {
theme_file: Some(PathBuf::from("config.toml")),
..Default::default()
};
args.merge_config(cfg);
assert_eq!(
args.theme_file,
Some(ThemeFileValue::Cli(PathBuf::from("cli.toml")))
);
}
#[test]
fn test_tui_parse_theme_file_from_cli() {
let args = TestCli::<TuiModeArgs>::parse_from(["test", "--theme", "cli.toml"]).args;
assert_eq!(
args.theme_file,
Some(ThemeFileValue::Cli(PathBuf::from("cli.toml")))
);
}
#[test]
fn test_tui_parse_theme_file_unset_by_default() {
let args = TestCli::<TuiModeArgs>::parse_from(["test"]).args;
assert_eq!(args.theme_file, None);
}
#[test]
fn test_tui_merge_config_inline_theme() {
use crate::cli::tui_theme::{
StyleSpec,
ThemeColor,
ThemeSpec,
};
let mut args = TuiModeArgs::default();
let cfg = TuiModeConfig {
theme: Some(ThemeSpec {
app_title: Some(StyleSpec {
fg: Some(ThemeColor::Named("cyan".into())),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
args.merge_config(cfg);
assert!(matches!(
args
.theme
.as_ref()
.and_then(|s| s.app_title.as_ref())
.and_then(|a| a.fg.as_ref()),
Some(ThemeColor::Named(s)) if s == "cyan"
));
}
#[test]
fn test_tui_validate_pty_options() {
let args = TuiModeArgs {
active_pane: Some(crate::cli::options::ActivePane::Terminal),
layout: Some(crate::cli::options::AppLayout::Vertical),
scrollback_lines: Some(2000),
..Default::default()
};
assert!(args.validate_pty_options(true).is_ok());
assert_that!(args.validate_pty_options(false), err(anything()));
assert_that!(
TuiModeArgs {
scrollback_lines: Some(1000),
..Default::default()
}
.validate_pty_options(false),
err(anything())
);
assert!(
TuiModeArgs {
active_pane: Some(crate::cli::options::ActivePane::Events),
..Default::default()
}
.validate_pty_options(false)
.is_ok()
);
assert!(
TuiModeArgs {
layout: Some(crate::cli::options::AppLayout::Vertical),
..Default::default()
}
.validate_pty_options(false)
.is_ok()
);
assert!(TuiModeArgs::default().validate_pty_options(false).is_ok());
}
#[test]
fn test_tui_cli_parse() {
let cli = TestCli::<TuiModeArgs>::parse_from(["test", "--follow", "--frame-rate", "30"]);
assert!(cli.args.tty());
assert!(!cli.args.no_tty);
assert!(cli.args.follow);
assert_eq!(cli.args.frame_rate, Some(30.0));
}
#[test]
fn test_tui_cli_parse_no_tty() {
let cli = TestCli::<TuiModeArgs>::parse_from(["test", "--no-tty"]);
assert!(!cli.args.tty());
assert!(cli.args.no_tty);
}
#[test]
fn test_tui_cli_no_tty_conflicts_with_terminal_options() {
let result =
TestCli::<TuiModeArgs>::try_parse_from(["test", "--no-tty", "--active-pane", "terminal"]);
assert_that!(result, err(anything()));
}
#[test]
fn test_tui_cli_exit_handling_overrides_config() {
let mut args = TuiModeArgs {
terminate_on_exit: true,
..Default::default()
};
let cfg = TuiModeConfig {
exit_handling: Some(ExitHandling::Kill),
..Default::default()
};
args.merge_config(cfg);
assert!(args.terminate_on_exit);
assert!(!args.kill_on_exit);
}
#[test]
fn test_debugger_merge_config() {
let mut args = DebuggerArgs::default();
let cfg = DebuggerConfig {
default_external_command: Some("echo hi".into()),
};
args.merge_config(cfg);
assert_eq!(args.default_external_command.as_deref(), Some("echo hi"));
}
#[test]
fn test_debugger_cli_parse_breakpoint() {
let cli = TestCli::<DebuggerArgs>::parse_from([
"test",
"--add-breakpoint",
"sysenter:exact-filename:/bin/ls",
]);
assert_eq!(cli.args.breakpoints.len(), 1);
}
#[test]
fn test_debugger_cli_command_overrides_config() {
let mut args = DebuggerArgs {
default_external_command: Some("cli-cmd".into()),
..Default::default()
};
let cfg = DebuggerConfig {
default_external_command: Some("config-cmd".into()),
};
args.merge_config(cfg);
assert_eq!(args.default_external_command.as_deref(), Some("cli-cmd"));
}
#[test]
fn test_frame_rate_parser_valid() {
assert_eq!(frame_rate_parser("60").unwrap(), 60.0);
}
#[test]
fn test_frame_rate_parser_too_low() {
let err = frame_rate_parser("1").unwrap_err();
let msg = err.to_string();
assert_that!(msg, contains_substring("too low"));
}
#[test]
fn test_frame_rate_parser_invalid() {
let err = frame_rate_parser("-1").unwrap_err();
assert_that!(err.to_string(), contains_substring("Invalid"));
}
#[test]
fn test_exporter_cli_parse() {
let cli = TestCli::<ExporterArgs>::parse_from(["test", "--pretty"]);
assert!(cli.args.pretty);
}
}