Skip to main content

tracexec_core/
cli.rs

1use std::{
2  io::{
3    BufWriter,
4    stderr,
5    stdout,
6  },
7  path::PathBuf,
8};
9
10use args::{
11  DebuggerArgs,
12  PtraceArgs,
13  TuiModeArgs,
14};
15use clap::{
16  CommandFactory,
17  Parser,
18  Subcommand,
19};
20use config::Config;
21use options::ExportFormat;
22use tracing::debug;
23
24use self::{
25  args::{
26    LogModeArgs,
27    ModifierArgs,
28    TracerEventArgs,
29  },
30  options::Color,
31};
32use crate::{
33  cli::args::ExporterArgs,
34  output::Output,
35};
36
37pub mod args;
38pub mod config;
39pub mod keys;
40pub mod options;
41pub mod theme;
42pub mod tui_theme;
43
44#[derive(Parser, Debug)]
45#[clap(
46  name = "tracexec",
47  author,
48  version,
49  about = "A small utility for tracing execve{,at} and pre-exec behavior."
50)]
51pub struct Cli {
52  #[arg(long, default_value_t = Color::Auto, help = "Control whether colored output is enabled. This flag has no effect on TUI mode.")]
53  pub color: Color,
54  #[arg(
55    short = 'C',
56    long,
57    help = "Change current directory to this path before doing anything"
58  )]
59  pub cwd: Option<PathBuf>,
60  #[arg(
61    short = 'P',
62    long,
63    help = "Load profile from this path",
64    conflicts_with = "no_profile"
65  )]
66  pub profile: Option<PathBuf>,
67  #[arg(long, help = "Do not load profiles")]
68  pub no_profile: bool,
69  #[arg(
70    short,
71    long,
72    help = "Run as user. This option is only available when running tracexec as root",
73    conflicts_with = "elevate"
74  )]
75  pub user: Option<String>,
76  #[arg(
77    long,
78    help = "Re-execute tracexec with privilege elevation (e.g. via sudo). \
79            The original user credentials are saved and the tracee will run as the original user.",
80    conflicts_with = "user"
81  )]
82  pub elevate: bool,
83  #[arg(
84    long,
85    hide = true,
86    conflicts_with = "elevate",
87    requires = "user",
88    help = "Internal: abstract Unix socket name used to request the original environment from --elevate"
89  )]
90  pub restore_env_socket: Option<String>,
91  #[arg(
92    long,
93    hide = true,
94    conflicts_with = "elevate",
95    requires_all = ["elevated_data_dir", "elevated_data_local_dir"],
96    help = "Internal: original user's config directory from --elevate"
97  )]
98  pub elevated_config_dir: Option<PathBuf>,
99  #[arg(
100    long,
101    hide = true,
102    conflicts_with = "elevate",
103    requires_all = ["elevated_config_dir", "elevated_data_local_dir"],
104    help = "Internal: original user's data directory from --elevate"
105  )]
106  pub elevated_data_dir: Option<PathBuf>,
107  #[arg(
108    long,
109    hide = true,
110    conflicts_with = "elevate",
111    requires_all = ["elevated_config_dir", "elevated_data_dir"],
112    help = "Internal: original user's local data directory from --elevate"
113  )]
114  pub elevated_data_local_dir: Option<PathBuf>,
115  #[clap(subcommand)]
116  pub cmd: CliCommand,
117}
118
119#[derive(Subcommand, Debug)]
120pub enum CliCommand {
121  #[clap(about = "Run tracexec in logging mode")]
122  Log {
123    #[arg(last = true, required = true, help = "command to be executed")]
124    cmd: Vec<String>,
125    #[clap(flatten)]
126    tracing_args: LogModeArgs,
127    #[clap(flatten)]
128    modifier_args: ModifierArgs,
129    #[clap(flatten)]
130    ptrace_args: PtraceArgs,
131    #[clap(flatten)]
132    tracer_event_args: TracerEventArgs,
133    #[clap(
134      short,
135      long,
136      help = "Output, stderr by default. A single hyphen '-' represents stdout."
137    )]
138    output: Option<PathBuf>,
139  },
140  #[clap(about = "Run tracexec in TUI mode with a pseudo terminal by default")]
141  Tui {
142    #[arg(last = true, required = true, help = "command to be executed")]
143    cmd: Vec<String>,
144    #[clap(flatten)]
145    modifier_args: ModifierArgs,
146    #[clap(flatten)]
147    ptrace_args: PtraceArgs,
148    #[clap(flatten)]
149    tracer_event_args: TracerEventArgs,
150    #[clap(flatten)]
151    tui_args: TuiModeArgs,
152    #[clap(flatten)]
153    debugger_args: DebuggerArgs,
154  },
155  #[clap(about = "Generate shell completions for tracexec")]
156  GenerateCompletions {
157    #[arg(required = true, help = "The shell to generate completions for")]
158    shell: clap_complete::Shell,
159  },
160  #[clap(about = "Collect exec events and export them")]
161  Collect {
162    #[arg(last = true, required = true, help = "command to be executed")]
163    cmd: Vec<String>,
164    #[clap(flatten)]
165    modifier_args: ModifierArgs,
166    #[clap(flatten)]
167    ptrace_args: PtraceArgs,
168    #[clap(flatten)]
169    exporter_args: ExporterArgs,
170    #[clap(short = 'F', long, help = "the format for exported exec events")]
171    format: ExportFormat,
172    #[clap(
173      short,
174      long,
175      help = "Output, stderr by default. A single hyphen '-' represents stdout."
176    )]
177    output: Option<PathBuf>,
178    #[clap(
179      long,
180      help = "Set the terminal foreground process group to tracee. This option is useful when tracexec is used interactively. [default]",
181      conflicts_with = "no_foreground"
182    )]
183    foreground: bool,
184    #[clap(
185      long,
186      help = "Do not set the terminal foreground process group to tracee",
187      conflicts_with = "foreground"
188    )]
189    no_foreground: bool,
190  },
191  #[cfg(feature = "ebpf")]
192  #[clap(about = "Experimental ebpf mode")]
193  Ebpf {
194    #[clap(subcommand)]
195    command: EbpfCommand,
196  },
197}
198
199#[derive(Subcommand, Debug)]
200#[cfg(feature = "ebpf")]
201pub enum EbpfCommand {
202  #[clap(about = "Run tracexec in logging mode")]
203  Log {
204    #[arg(
205      last = true,
206      help = "command to be executed. Leave it empty to trace all exec on system"
207    )]
208    cmd: Vec<String>,
209    #[clap(
210      short,
211      long,
212      help = "Output, stderr by default. A single hyphen '-' represents stdout."
213    )]
214    output: Option<PathBuf>,
215    #[clap(flatten)]
216    modifier_args: ModifierArgs,
217    #[clap(flatten)]
218    log_args: LogModeArgs,
219  },
220  #[clap(about = "Run tracexec in TUI mode, with a pseudo terminal when following a command")]
221  Tui {
222    #[arg(
223      last = true,
224      help = "command to be executed. Leave it empty to trace all exec on system"
225    )]
226    cmd: Vec<String>,
227    #[clap(flatten)]
228    modifier_args: ModifierArgs,
229    #[clap(flatten)]
230    tracer_event_args: TracerEventArgs,
231    #[clap(flatten)]
232    tui_args: TuiModeArgs,
233  },
234  #[clap(about = "Collect exec events and export them")]
235  Collect {
236    #[arg(
237      last = true,
238      help = "command to be executed. Leave it empty to trace all exec on system"
239    )]
240    cmd: Vec<String>,
241    #[clap(flatten)]
242    modifier_args: ModifierArgs,
243    #[clap(short = 'F', long, help = "the format for exported exec events")]
244    format: ExportFormat,
245    #[clap(flatten)]
246    exporter_args: ExporterArgs,
247    #[clap(
248      short,
249      long,
250      help = "Output, stderr by default. A single hyphen '-' represents stdout."
251    )]
252    output: Option<PathBuf>,
253    #[clap(
254      long,
255      help = "Set the terminal foreground process group to tracee. This option is useful when tracexec is used interactively. [default]",
256      conflicts_with = "no_foreground"
257    )]
258    foreground: bool,
259    #[clap(
260      long,
261      help = "Do not set the terminal foreground process group to tracee",
262      conflicts_with = "foreground"
263    )]
264    no_foreground: bool,
265  },
266}
267
268impl Cli {
269  pub fn get_output(path: Option<PathBuf>, color: Color) -> std::io::Result<Box<Output>> {
270    Ok(match path {
271      None => Box::new(stderr()),
272      Some(ref x) if x.as_os_str() == "-" => Box::new(stdout()),
273      Some(path) => {
274        let file = std::fs::OpenOptions::new()
275          .create(true)
276          .truncate(true)
277          .write(true)
278          .open(path)?;
279        if color != Color::Always {
280          // Disable color by default when output is file
281          owo_colors::control::set_should_colorize(false);
282        }
283        Box::new(BufWriter::new(file))
284      }
285    })
286  }
287
288  pub fn generate_completions(shell: clap_complete::Shell) {
289    let mut cmd = Self::command();
290    clap_complete::generate(shell, &mut cmd, env!("CARGO_CRATE_NAME"), &mut stdout())
291  }
292
293  pub fn merge_config(&mut self, config: Config) {
294    debug!("Merging config: {config:?}");
295    match &mut self.cmd {
296      CliCommand::Log {
297        tracing_args,
298        modifier_args,
299        ptrace_args,
300        ..
301      } => {
302        if let Some(c) = config.ptrace {
303          ptrace_args.merge_config(c);
304        }
305        if let Some(c) = config.modifier {
306          modifier_args.merge_config(c);
307        }
308        if let Some(c) = config.log {
309          tracing_args.merge_config(c);
310        }
311      }
312      CliCommand::Tui {
313        modifier_args,
314        ptrace_args,
315        tui_args,
316        debugger_args,
317        ..
318      } => {
319        if let Some(c) = config.ptrace {
320          ptrace_args.merge_config(c);
321        }
322        if let Some(c) = config.modifier {
323          modifier_args.merge_config(c);
324        }
325        if let Some(c) = config.tui {
326          tui_args.merge_config(c);
327        }
328        if let Some(c) = config.debugger {
329          debugger_args.merge_config(c);
330        }
331      }
332      CliCommand::Collect {
333        foreground,
334        no_foreground,
335        ptrace_args,
336        ..
337      } => {
338        if let Some(c) = config.ptrace {
339          ptrace_args.merge_config(c);
340        }
341        if let Some(c) = config.log
342          && (!*foreground)
343          && (!*no_foreground)
344          && let Some(x) = c.foreground
345        {
346          if x {
347            *foreground = true;
348          } else {
349            *no_foreground = true;
350          }
351        }
352      }
353      #[cfg(feature = "ebpf")]
354      CliCommand::Ebpf { command } => command.merge_config(config),
355      CliCommand::GenerateCompletions { .. } => (),
356    }
357  }
358}
359
360#[cfg(feature = "ebpf")]
361impl EbpfCommand {
362  fn merge_config(&mut self, config: Config) {
363    let modifier_args = match self {
364      Self::Log { modifier_args, .. }
365      | Self::Tui { modifier_args, .. }
366      | Self::Collect { modifier_args, .. } => modifier_args,
367    };
368    if let Some(c) = config.modifier {
369      modifier_args.merge_config(c);
370    }
371    match self {
372      Self::Log { log_args, .. } => {
373        if let Some(c) = config.log {
374          log_args.merge_config(c);
375        }
376      }
377      Self::Tui { tui_args, .. } => {
378        if let Some(c) = config.tui {
379          tui_args.merge_config(c);
380        }
381      }
382      Self::Collect {
383        foreground,
384        no_foreground,
385        ..
386      } => {
387        if let Some(c) = config.log
388          && (!*foreground)
389          && (!*no_foreground)
390          && let Some(x) = c.foreground
391        {
392          if x {
393            *foreground = true;
394          } else {
395            *no_foreground = true;
396          }
397        }
398      }
399    }
400  }
401}
402
403#[cfg(test)]
404mod tests {
405  use std::{
406    fs,
407    io::Write,
408    path::PathBuf,
409  };
410
411  use test_that::prelude::*;
412
413  use super::*;
414  use crate::cli::{
415    args::{
416      DebuggerArgs,
417      LogModeArgs,
418      ModifierArgs,
419      PtraceArgs,
420      TracerEventArgs,
421      TuiModeArgs,
422    },
423    config::{
424      Config,
425      DebuggerConfig,
426      LogModeConfig,
427      ModifierConfig,
428      PtraceConfig,
429      TuiModeConfig,
430    },
431    options::{
432      Color,
433      ExportFormat,
434      SeccompBpf,
435    },
436  };
437
438  #[test]
439  fn test_cli_parse_log() {
440    let args = vec![
441      "tracexec",
442      "log",
443      "--show-interpreter",
444      "--successful-only",
445      "--",
446      "echo",
447      "hello",
448    ];
449    let cli = Cli::parse_from(args);
450
451    if let CliCommand::Log {
452      cmd,
453      tracing_args,
454      modifier_args,
455      ..
456    } = cli.cmd
457    {
458      assert_eq!(cmd, vec!["echo", "hello"]);
459      assert!(tracing_args.show_interpreter);
460      assert!(modifier_args.successful_only);
461    } else {
462      panic!("Expected Log command");
463    }
464  }
465
466  #[test]
467  fn test_cli_parse_tui() {
468    let args = vec!["tracexec", "tui", "--follow", "--", "bash"];
469    let cli = Cli::parse_from(args);
470
471    if let CliCommand::Tui { cmd, tui_args, .. } = cli.cmd {
472      assert_eq!(cmd, vec!["bash"]);
473      assert!(tui_args.tty());
474      assert!(tui_args.follow);
475    } else {
476      panic!("Expected Tui command");
477    }
478  }
479
480  #[test]
481  fn test_cli_parse_elevate_tui() {
482    let args = vec!["tracexec", "--elevate", "tui", "--", "sudo", "ls"];
483    let cli = Cli::parse_from(args);
484    assert!(cli.elevate);
485    assert_that!(cli.user, none());
486    if let CliCommand::Tui { cmd, .. } = cli.cmd {
487      assert_eq!(cmd, vec!["sudo", "ls"]);
488    } else {
489      panic!("Expected Tui command");
490    }
491  }
492
493  #[test]
494  fn test_cli_parse_elevate_log() {
495    let args = vec!["tracexec", "--elevate", "log", "--", "ls"];
496    let cli = Cli::parse_from(args);
497    assert!(cli.elevate);
498    if let CliCommand::Log { cmd, .. } = cli.cmd {
499      assert_eq!(cmd, vec!["ls"]);
500    } else {
501      panic!("Expected Log command");
502    }
503  }
504
505  #[test]
506  fn test_cli_parse_elevate_conflicts_with_user() {
507    let args = vec!["tracexec", "--elevate", "--user", "root", "log", "--", "ls"];
508    let result = Cli::try_parse_from(args);
509    assert_that!(result, err(anything()));
510  }
511
512  #[test]
513  fn test_cli_parse_restore_env_socket_requires_user() {
514    let args = vec![
515      "tracexec",
516      "--restore-env-socket",
517      "socket-name",
518      "log",
519      "--",
520      "ls",
521    ];
522    let result = Cli::try_parse_from(args);
523    assert_that!(result, err(anything()));
524  }
525
526  #[test]
527  fn test_cli_parse_tui_theme_file_cli_source() {
528    let args = vec![
529      "tracexec",
530      "--no-profile",
531      "tui",
532      "--theme",
533      "cli.toml",
534      "--",
535      "bash",
536    ];
537    let cli = Cli::parse_from(args);
538
539    assert!(cli.no_profile);
540    if let CliCommand::Tui { tui_args, .. } = cli.cmd {
541      assert_eq!(
542        tui_args.theme_file,
543        Some(crate::cli::args::ThemeFileValue::Cli(PathBuf::from(
544          "cli.toml"
545        )))
546      );
547    } else {
548      panic!("Expected Tui command");
549    }
550  }
551
552  #[test]
553  fn test_get_output_stderr_stdout_file() {
554    // default (None) -> stderr
555    let out = Cli::get_output(None, Color::Auto).unwrap();
556    let _ = out; // just ensure it returns something
557
558    // "-" -> stdout
559    let out = Cli::get_output(Some(PathBuf::from("-")), Color::Auto).unwrap();
560    let _ = out;
561
562    // real file
563    let dir = tempfile::tempdir().unwrap();
564    let path = dir.path().join("test_output.txt");
565    let mut out = Cli::get_output(Some(path.clone()), Color::Auto).unwrap();
566    writeln!(out, "Hello world").unwrap();
567    drop(out);
568
569    let content = fs::read_to_string(path).unwrap();
570    assert_that!(content, contains_substring("Hello world"));
571  }
572
573  #[test]
574  fn test_merge_config_log() {
575    let mut cli = Cli {
576      color: Color::Auto,
577      cwd: None,
578      profile: None,
579      no_profile: false,
580      user: None,
581      elevate: false,
582      restore_env_socket: None,
583      elevated_config_dir: None,
584      elevated_data_dir: None,
585      elevated_data_local_dir: None,
586      cmd: CliCommand::Log {
587        cmd: vec!["ls".into()],
588        tracing_args: LogModeArgs {
589          show_interpreter: false,
590          ..Default::default()
591        },
592        modifier_args: ModifierArgs::default(),
593        ptrace_args: PtraceArgs::default(),
594        tracer_event_args: TracerEventArgs::all(),
595        output: None,
596      },
597    };
598
599    let config = Config {
600      ptrace: Some(PtraceConfig {
601        seccomp_bpf: Some(SeccompBpf::On),
602      }),
603      modifier: Some(ModifierConfig {
604        successful_only: Some(true),
605        ..Default::default()
606      }),
607      log: Some(LogModeConfig {
608        show_interpreter: Some(true),
609        ..Default::default()
610      }),
611      tui: None,
612      debugger: None,
613    };
614
615    cli.merge_config(config);
616
617    if let CliCommand::Log {
618      tracing_args,
619      modifier_args,
620      ptrace_args,
621      ..
622    } = cli.cmd
623    {
624      assert!(tracing_args.show_interpreter);
625      assert!(modifier_args.successful_only);
626      assert_eq!(ptrace_args.seccomp_bpf, SeccompBpf::On);
627    } else {
628      panic!("Expected Log command");
629    }
630  }
631
632  #[test]
633  fn test_merge_config_tui() {
634    let mut cli = Cli {
635      color: Color::Auto,
636      cwd: None,
637      profile: None,
638      no_profile: false,
639      user: None,
640      elevate: false,
641      restore_env_socket: None,
642      elevated_config_dir: None,
643      elevated_data_dir: None,
644      elevated_data_local_dir: None,
645      cmd: CliCommand::Tui {
646        cmd: vec!["bash".into()],
647        modifier_args: ModifierArgs::default(),
648        ptrace_args: PtraceArgs::default(),
649        tracer_event_args: TracerEventArgs::all(),
650        tui_args: TuiModeArgs::default(),
651        debugger_args: DebuggerArgs::default(),
652      },
653    };
654
655    let config = Config {
656      ptrace: Some(PtraceConfig {
657        seccomp_bpf: Some(SeccompBpf::Off),
658      }),
659      modifier: Some(ModifierConfig {
660        successful_only: Some(true),
661        ..Default::default()
662      }),
663      log: None,
664      tui: Some(TuiModeConfig {
665        follow: Some(true),
666        frame_rate: Some(30.0),
667        ..Default::default()
668      }),
669      debugger: Some(DebuggerConfig {
670        default_external_command: Some("echo hello".into()),
671      }),
672    };
673
674    cli.merge_config(config);
675
676    if let CliCommand::Tui {
677      modifier_args,
678      ptrace_args,
679      tui_args,
680      debugger_args,
681      ..
682    } = cli.cmd
683    {
684      assert!(modifier_args.successful_only);
685      assert_eq!(ptrace_args.seccomp_bpf, SeccompBpf::Off);
686      assert_eq!(tui_args.frame_rate.unwrap(), 30.0);
687      assert_eq!(
688        debugger_args.default_external_command.as_ref().unwrap(),
689        "echo hello"
690      );
691    } else {
692      panic!("Expected Tui command");
693    }
694  }
695
696  #[test]
697  fn test_merge_config_collect_foreground() {
698    let mut cli = Cli {
699      color: Color::Auto,
700      cwd: None,
701      profile: None,
702      no_profile: false,
703      user: None,
704      elevate: false,
705      restore_env_socket: None,
706      elevated_config_dir: None,
707      elevated_data_dir: None,
708      elevated_data_local_dir: None,
709      cmd: CliCommand::Collect {
710        cmd: vec!["ls".into()],
711        modifier_args: ModifierArgs::default(),
712        ptrace_args: PtraceArgs::default(),
713        exporter_args: Default::default(),
714        format: ExportFormat::Json,
715        output: None,
716        foreground: false,
717        no_foreground: false,
718      },
719    };
720
721    let config = Config {
722      log: Some(LogModeConfig {
723        foreground: Some(true),
724        ..Default::default()
725      }),
726      ptrace: None,
727      modifier: None,
728      tui: None,
729      debugger: None,
730    };
731
732    cli.merge_config(config);
733
734    if let CliCommand::Collect {
735      foreground,
736      no_foreground,
737      ..
738    } = cli.cmd
739    {
740      assert!(foreground);
741      assert!(!no_foreground);
742    } else {
743      panic!("Expected Collect command");
744    }
745  }
746
747  #[test]
748  fn test_generate_completions_smoke() {
749    // smoke test: just run without panicking
750    Cli::generate_completions(clap_complete::Shell::Bash);
751  }
752
753  #[cfg(feature = "ebpf")]
754  mod ebpf {
755    use super::*;
756    use crate::cli::args::ThemeFileValue;
757
758    fn load_profile(args: &[&str], profile: &str) -> color_eyre::Result<Cli> {
759      let mut file = tempfile::NamedTempFile::new()?;
760      file.write_all(profile.as_bytes())?;
761      let mut cli = Cli::try_parse_from(args)?;
762      cli.merge_config(Config::load(Some(file.path().to_path_buf()))?);
763      Ok(cli)
764    }
765
766    #[test]
767    fn test_merge_config_ebpf_log() -> color_eyre::Result<()> {
768      for with_command in [false, true] {
769        for override_config in [false, true] {
770          let mut args = vec!["tracexec", "ebpf", "log"];
771          if override_config {
772            args.extend(["--no-show-interpreter", "--foreground", "--no-timestamp"]);
773          }
774          if with_command {
775            args.extend(["--", "true"]);
776          }
777          let cli = load_profile(
778            &args,
779            r#"
780              [log]
781              show_interpreter = true
782              foreground = false
783              [modifier]
784              successful_only = true
785              timestamp = { enable = true }
786            "#,
787          )?;
788          let CliCommand::Ebpf {
789            command:
790              EbpfCommand::Log {
791                log_args,
792                modifier_args,
793                ..
794              },
795          } = cli.cmd
796          else {
797            panic!("Expected eBPF Log command");
798          };
799          assert_eq!(log_args.show_interpreter, !override_config);
800          assert_eq!(log_args.foreground(), override_config);
801          let modifier_args = modifier_args.processed();
802          assert!(modifier_args.successful_only);
803          assert_eq!(modifier_args.timestamp, !override_config);
804        }
805      }
806      Ok(())
807    }
808
809    #[test]
810    fn test_merge_config_ebpf_tui() -> color_eyre::Result<()> {
811      for with_command in [false, true] {
812        for override_config in [false, true] {
813          let mut args = vec!["tracexec", "ebpf", "tui"];
814          if override_config {
815            args.extend([
816              "--frame-rate",
817              "45",
818              "--theme",
819              "cli.toml",
820              "--no-timestamp",
821            ]);
822          }
823          if with_command {
824            args.extend(["--", "true"]);
825          }
826          let cli = load_profile(
827            &args,
828            r#"
829              [tui]
830              follow = true
831              frame_rate = 30.0
832              theme-file = "profile.toml"
833              [tui.keys]
834              quit = "q"
835              [modifier]
836              timestamp = { enable = true }
837            "#,
838          )?;
839          let CliCommand::Ebpf {
840            command:
841              EbpfCommand::Tui {
842                tui_args,
843                modifier_args,
844                ..
845              },
846          } = cli.cmd
847          else {
848            panic!("Expected eBPF Tui command");
849          };
850          assert!(tui_args.follow);
851          assert_eq!(
852            tui_args.frame_rate,
853            Some(if override_config { 45.0 } else { 30.0 })
854          );
855          assert_eq!(
856            tui_args.theme_file,
857            Some(if override_config {
858              ThemeFileValue::Cli("cli.toml".into())
859            } else {
860              ThemeFileValue::Config("profile.toml".into())
861            })
862          );
863          assert!(tui_args.keys.is_some_and(|keys| keys.quit.is_some()));
864          assert_eq!(modifier_args.processed().timestamp, !override_config);
865        }
866      }
867      Ok(())
868    }
869
870    #[test]
871    fn test_merge_config_ebpf_collect() -> color_eyre::Result<()> {
872      for with_command in [false, true] {
873        for configured_foreground in [false, true] {
874          for cli_foreground in [None, Some(false), Some(true)] {
875            let mut args = vec!["tracexec", "ebpf", "collect", "--format", "json"];
876            if let Some(foreground) = cli_foreground {
877              args.push(if foreground {
878                "--foreground"
879              } else {
880                "--no-foreground"
881              });
882            }
883            if with_command {
884              args.extend(["--", "true"]);
885            }
886            let cli = load_profile(
887              &args,
888              &format!(
889                "[log]\nforeground = {configured_foreground}\n[modifier]\ncollect_cgroup = true"
890              ),
891            )?;
892            let CliCommand::Ebpf {
893              command:
894                EbpfCommand::Collect {
895                  foreground,
896                  no_foreground,
897                  modifier_args,
898                  ..
899                },
900            } = cli.cmd
901            else {
902              panic!("Expected eBPF Collect command");
903            };
904            let expected = cli_foreground.unwrap_or(configured_foreground);
905            assert_eq!((foreground, no_foreground), (expected, !expected));
906            assert!(modifier_args.processed().collect_cgroup);
907          }
908        }
909      }
910      Ok(())
911    }
912  }
913}