iai_callgrind_runner/runner/tool/
mod.rs

1// spell-checker: ignore extbase extbasename extold
2pub mod args;
3pub mod error_metric_parser;
4pub mod generic_parser;
5pub mod logfile_parser;
6
7use std::collections::HashMap;
8use std::ffi::OsString;
9use std::fmt::{Display, Write as FmtWrite};
10use std::fs::{DirEntry, File};
11use std::io::{stderr, BufRead, BufReader, Write};
12use std::os::unix::fs::MetadataExt;
13use std::path::{Path, PathBuf};
14use std::process::{Child, Command, ExitStatus, Output};
15
16use anyhow::{anyhow, Context, Result};
17use lazy_static::lazy_static;
18use log::{debug, error, log_enabled};
19use logfile_parser::Logfile;
20use regex::Regex;
21#[cfg(feature = "schema")]
22use schemars::JsonSchema;
23use serde::{Deserialize, Serialize};
24
25use self::args::ToolArgs;
26use super::args::NoCapture;
27use super::bin_bench::Delay;
28use super::callgrind::parser::parse_header;
29use super::common::{Assistant, Config, ModulePath, Sandbox};
30use super::format::{print_no_capture_footer, Formatter, OutputFormat, VerticalFormatter};
31use super::meta::Metadata;
32use super::summary::{BaselineKind, ToolRun, ToolSummary};
33use crate::api::{self, ExitWith, Stream};
34use crate::error::Error;
35use crate::util::{self, resolve_binary_path, truncate_str_utf8, EitherOrBoth};
36
37lazy_static! {
38    // This regex matches the original file name without the prefix as it is created by callgrind.
39    // The baseline <name> (base@<name>) can only consist of ascii and underscore characters.
40    // Flamegraph files are ignored by this regex
41    static ref CALLGRIND_ORIG_FILENAME_RE: Regex = Regex::new(
42        r"^(?<type>[.](out|log))(?<base>[.](old|base@[^.-]+))?(?<pid>[.][#][0-9]+)?(?<part>[.][0-9]+)?(?<thread>-[0-9]+)?$"
43    )
44    .expect("Regex should compile");
45
46    /// This regex matches the original file name without the prefix as it is created by bbv
47    static ref BBV_ORIG_FILENAME_RE: Regex = Regex::new(
48        r"^(?<type>[.](?:out|log))(?<base>[.](old|base@[^.]+))?(?<bbv_type>[.](?:bb|pc))?(?<pid>[.][#][0-9]+)?(?<thread>[.][0-9]+)?$"
49    )
50    .expect("Regex should compile");
51
52    /// This regex matches the original file name without the prefix as it is created by all tools
53    /// other than callgrind and bbv.
54    static ref GENERIC_ORIG_FILENAME_RE: Regex = Regex::new(
55        r"^(?<type>[.](?:out|log))(?<base>[.](old|base@[^.]+))?(?<pid>[.][#][0-9]+)?$"
56    )
57    .expect("Regex should compile");
58}
59
60#[derive(Debug, Default, Clone)]
61pub struct RunOptions {
62    pub env_clear: bool,
63    pub current_dir: Option<PathBuf>,
64    pub exit_with: Option<ExitWith>,
65    pub envs: Vec<(OsString, OsString)>,
66    pub stdin: Option<api::Stdin>,
67    pub stdout: Option<api::Stdio>,
68    pub stderr: Option<api::Stdio>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct ToolConfig {
73    pub tool: ValgrindTool,
74    pub is_enabled: bool,
75    pub args: ToolArgs,
76    pub outfile_modifier: Option<String>,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct ToolConfigs(pub Vec<ToolConfig>);
81
82pub struct ToolCommand {
83    tool: ValgrindTool,
84    nocapture: NoCapture,
85    command: Command,
86}
87
88pub struct ToolOutput {
89    pub tool: ValgrindTool,
90    pub output: Option<Output>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct ToolOutputPath {
95    pub kind: ToolOutputPathKind,
96    pub tool: ValgrindTool,
97    pub baseline_kind: BaselineKind,
98    /// The final directory of all the output files
99    pub dir: PathBuf,
100    pub name: String,
101    pub modifiers: Vec<String>,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum ToolOutputPathKind {
106    Out,
107    OldOut,
108    Log,
109    OldLog,
110    BaseLog(String),
111    Base(String),
112}
113
114/// All currently available valgrind tools
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
116#[cfg_attr(feature = "schema", derive(JsonSchema))]
117pub enum ValgrindTool {
118    Callgrind,
119    Memcheck,
120    Helgrind,
121    DRD,
122    Massif,
123    DHAT,
124    BBV,
125}
126
127impl ToolCommand {
128    pub fn new(tool: ValgrindTool, meta: &Metadata, nocapture: NoCapture) -> Self {
129        Self {
130            tool,
131            nocapture,
132            command: meta.into(),
133        }
134    }
135
136    pub fn env_clear(&mut self) -> &mut Self {
137        debug!("{}: Clearing environment variables", self.tool.id());
138        for (key, _) in std::env::vars() {
139            match (key.as_str(), self.tool) {
140                (key @ ("DEBUGINFOD_URLS" | "PATH" | "HOME"), ValgrindTool::Memcheck)
141                | (key @ ("LD_PRELOAD" | "LD_LIBRARY_PATH"), _) => {
142                    debug!(
143                        "{}: Clearing environment variables: Skipping {key}",
144                        self.tool.id()
145                    );
146                }
147                _ => {
148                    self.command.env_remove(key);
149                }
150            }
151        }
152        self
153    }
154
155    pub fn run(
156        mut self,
157        config: ToolConfig,
158        executable: &Path,
159        executable_args: &[OsString],
160        run_options: RunOptions,
161        output_path: &ToolOutputPath,
162        module_path: &ModulePath,
163        mut child: Option<Child>,
164    ) -> Result<ToolOutput> {
165        debug!(
166            "{}: Running with executable '{}'",
167            self.tool.id(),
168            executable.display()
169        );
170
171        let RunOptions {
172            env_clear,
173            current_dir,
174            exit_with,
175            envs,
176            stdin,
177            stdout,
178            stderr,
179        } = run_options;
180
181        if env_clear {
182            debug!("Clearing environment variables");
183            self.env_clear();
184        }
185
186        if let Some(dir) = current_dir {
187            debug!(
188                "{}: Setting current directory to '{}'",
189                self.tool.id(),
190                dir.display()
191            );
192            self.command.current_dir(dir);
193        }
194
195        let mut tool_args = config.args;
196        tool_args.set_output_arg(output_path, config.outfile_modifier.as_ref());
197        tool_args.set_log_arg(output_path, config.outfile_modifier.as_ref());
198
199        let executable = resolve_binary_path(executable)?;
200        let args = tool_args.to_vec();
201        debug!(
202            "{}: Arguments: {}",
203            self.tool.id(),
204            args.iter()
205                .map(|s| s.to_string_lossy().to_string())
206                .collect::<Vec<String>>()
207                .join(" ")
208        );
209
210        self.command
211            .args(tool_args.to_vec())
212            .arg(&executable)
213            .args(executable_args)
214            .envs(envs);
215
216        if self.tool == ValgrindTool::Callgrind {
217            debug!("Applying --nocapture options");
218            self.nocapture.apply(&mut self.command);
219        }
220
221        if let Some(stdin) = stdin {
222            stdin
223                .apply(&mut self.command, Stream::Stdin, child.as_mut())
224                .map_err(|error| {
225                    Error::BenchmarkError(ValgrindTool::Callgrind, module_path.clone(), error)
226                })?;
227        }
228
229        if let Some(stdout) = stdout {
230            stdout
231                .apply(&mut self.command, Stream::Stdout)
232                .map_err(|error| Error::BenchmarkError(self.tool, module_path.clone(), error))?;
233        }
234
235        if let Some(stderr) = stderr {
236            stderr
237                .apply(&mut self.command, Stream::Stderr)
238                .map_err(|error| Error::BenchmarkError(self.tool, module_path.clone(), error))?;
239        }
240
241        let output = match self.nocapture {
242            NoCapture::True | NoCapture::Stderr | NoCapture::Stdout
243                if self.tool == ValgrindTool::Callgrind =>
244            {
245                self.command
246                    .status()
247                    .map_err(|error| {
248                        Error::LaunchError(PathBuf::from("valgrind"), error.to_string()).into()
249                    })
250                    .and_then(|status| {
251                        check_exit(
252                            self.tool,
253                            &executable,
254                            None,
255                            status,
256                            &output_path.to_log_output(),
257                            exit_with.as_ref(),
258                        )
259                    })?;
260                None
261            }
262            _ => self
263                .command
264                .output()
265                .map_err(|error| {
266                    Error::LaunchError(PathBuf::from("valgrind"), error.to_string()).into()
267                })
268                .and_then(|output| {
269                    let status = output.status;
270                    check_exit(
271                        self.tool,
272                        &executable,
273                        Some(output),
274                        status,
275                        &output_path.to_log_output(),
276                        exit_with.as_ref(),
277                    )
278                })?,
279        };
280
281        if let Some(mut child) = child {
282            debug!("Waiting for setup child process");
283            let status = child.wait().expect("Setup child process should have run");
284            if !status.success() {
285                return Err(Error::ProcessError((
286                    module_path.join("setup").to_string(),
287                    None,
288                    status,
289                    None,
290                ))
291                .into());
292            }
293        }
294
295        output_path.sanitize()?;
296
297        Ok(ToolOutput {
298            tool: self.tool,
299            output,
300        })
301    }
302}
303
304impl ToolConfig {
305    pub fn new<T>(tool: ValgrindTool, is_enabled: bool, args: T, modifier: Option<String>) -> Self
306    where
307        T: Into<ToolArgs>,
308    {
309        Self {
310            tool,
311            is_enabled,
312            args: args.into(),
313            outfile_modifier: modifier,
314        }
315    }
316
317    fn parse_load(
318        &self,
319        config: &Config,
320        log_path: &ToolOutputPath,
321        out_path: Option<&ToolOutputPath>,
322    ) -> Result<ToolSummary> {
323        let parser = logfile_parser::parser_factory(self.tool, config.meta.project_root.clone());
324
325        let parsed_new = parser.parse(log_path)?;
326        let parsed_old = parser.parse(&log_path.to_base_path())?;
327
328        let summaries = ToolRun::from(EitherOrBoth::Both(parsed_new, parsed_old));
329        Ok(ToolSummary {
330            tool: self.tool,
331            log_paths: log_path.real_paths()?,
332            out_paths: out_path.map_or_else(|| Ok(Vec::default()), ToolOutputPath::real_paths)?,
333            summaries,
334        })
335    }
336}
337
338impl TryFrom<api::Tool> for ToolConfig {
339    type Error = anyhow::Error;
340
341    fn try_from(value: api::Tool) -> std::result::Result<Self, Self::Error> {
342        let tool = value.kind.into();
343        ToolArgs::try_from_raw_args(tool, value.raw_args).map(|args| Self {
344            tool,
345            is_enabled: value.enable.unwrap_or(true),
346            args,
347            outfile_modifier: None,
348        })
349    }
350}
351
352impl ToolConfigs {
353    pub fn has_tools_enabled(&self) -> bool {
354        self.0.iter().any(|t| t.is_enabled)
355    }
356
357    pub fn output_paths(&self, output_path: &ToolOutputPath) -> Vec<ToolOutputPath> {
358        self.0
359            .iter()
360            .filter(|t| t.is_enabled)
361            .map(|t| output_path.to_tool_output(t.tool))
362            .collect()
363    }
364
365    fn print_headline(tool_config: &ToolConfig, output_format: &OutputFormat) {
366        if output_format.is_default() {
367            let mut formatter = VerticalFormatter::new(*output_format);
368            formatter.format_tool_headline(tool_config.tool);
369            formatter.print_buffer();
370        }
371    }
372
373    fn print(config: &Config, output_format: &OutputFormat, tool_run: &ToolRun) -> Result<()> {
374        VerticalFormatter::new(*output_format).print(config, (None, None), tool_run)
375    }
376
377    pub fn parse(
378        tool_config: &ToolConfig,
379        meta: &Metadata,
380        log_path: &ToolOutputPath,
381        out_path: Option<&ToolOutputPath>,
382        old_summaries: Vec<Logfile>,
383    ) -> Result<ToolSummary> {
384        let parser = logfile_parser::parser_factory(tool_config.tool, meta.project_root.clone());
385
386        let parsed_new = parser.parse(log_path)?;
387
388        let summaries = match (parsed_new.is_empty(), old_summaries.is_empty()) {
389            (true, false | true) => return Err(anyhow!("A new dataset should always be present")),
390            (false, true) => ToolRun::from(EitherOrBoth::Left(parsed_new)),
391            (false, false) => ToolRun::from(EitherOrBoth::Both(parsed_new, old_summaries)),
392        };
393
394        Ok(ToolSummary {
395            tool: tool_config.tool,
396            log_paths: log_path.real_paths()?,
397            out_paths: out_path.map_or_else(|| Ok(Vec::default()), ToolOutputPath::real_paths)?,
398            summaries,
399        })
400    }
401
402    pub fn run_loaded_vs_base(
403        &self,
404        config: &Config,
405        output_path: &ToolOutputPath,
406        output_format: &OutputFormat,
407    ) -> Result<Vec<ToolSummary>> {
408        let mut tool_summaries = vec![];
409        for tool_config in self.0.iter().filter(|t| t.is_enabled) {
410            Self::print_headline(tool_config, output_format);
411
412            let tool = tool_config.tool;
413
414            let output_path = output_path.to_tool_output(tool);
415            let log_path = output_path.to_log_output();
416
417            let tool_summary = tool_config.parse_load(config, &log_path, None)?;
418
419            Self::print(config, output_format, &tool_summary.summaries)?;
420
421            log_path.dump_log(log::Level::Info, &mut stderr())?;
422
423            tool_summaries.push(tool_summary);
424        }
425
426        Ok(tool_summaries)
427    }
428
429    pub fn run(
430        &self,
431        config: &Config,
432        executable: &Path,
433        executable_args: &[OsString],
434        run_options: &RunOptions,
435        output_path: &ToolOutputPath,
436        save_baseline: bool,
437        module_path: &ModulePath,
438        sandbox: Option<&api::Sandbox>,
439        setup: Option<&Assistant>,
440        teardown: Option<&Assistant>,
441        delay: Option<&Delay>,
442        output_format: &OutputFormat,
443    ) -> Result<Vec<ToolSummary>> {
444        let mut tool_summaries = vec![];
445        for tool_config in self.0.iter().filter(|t| t.is_enabled) {
446            // Print the headline as soon as possible, so if there are any errors, the errors shown
447            // in the terminal output can be associated with the tool
448            Self::print_headline(tool_config, output_format);
449
450            let tool = tool_config.tool;
451
452            let command = ToolCommand::new(tool, &config.meta, NoCapture::False);
453
454            let output_path = output_path.to_tool_output(tool);
455            let log_path = output_path.to_log_output();
456
457            let parser = logfile_parser::parser_factory(tool, config.meta.project_root.clone());
458
459            let old_summaries = parser.parse(&log_path.to_base_path())?;
460            if save_baseline {
461                output_path.clear()?;
462                log_path.clear()?;
463            }
464
465            let sandbox = sandbox
466                .map(|sandbox| Sandbox::setup(sandbox, &config.meta))
467                .transpose()?;
468
469            let mut child = setup
470                .as_ref()
471                .map_or(Ok(None), |setup| setup.run(config, module_path))?;
472
473            if let Some(delay) = delay {
474                if let Err(error) = delay.run() {
475                    if let Some(mut child) = child.take() {
476                        // To avoid zombies
477                        child.kill()?;
478                        return Err(error);
479                    }
480                }
481            }
482
483            let output = command.run(
484                tool_config.clone(),
485                executable,
486                executable_args,
487                run_options.clone(),
488                &output_path,
489                module_path,
490                child,
491            )?;
492
493            if let Some(teardown) = teardown {
494                teardown.run(config, module_path)?;
495            }
496
497            print_no_capture_footer(
498                NoCapture::False,
499                run_options.stdout.as_ref(),
500                run_options.stderr.as_ref(),
501            );
502
503            if let Some(sandbox) = sandbox {
504                sandbox.reset()?;
505            }
506
507            let tool_summary = Self::parse(
508                tool_config,
509                &config.meta,
510                &log_path,
511                tool.has_output_file().then_some(&output_path),
512                old_summaries,
513            )?;
514
515            Self::print(config, output_format, &tool_summary.summaries)?;
516
517            output.dump_log(log::Level::Info);
518            log_path.dump_log(log::Level::Info, &mut stderr())?;
519
520            tool_summaries.push(tool_summary);
521        }
522
523        Ok(tool_summaries)
524    }
525}
526
527impl ToolOutput {
528    pub fn dump_log(&self, log_level: log::Level) {
529        if let Some(output) = &self.output {
530            if log_enabled!(log_level) {
531                let (stdout, stderr) = (&output.stdout, &output.stderr);
532                if !stdout.is_empty() {
533                    log::log!(log_level, "{} output on stdout:", self.tool.id());
534                    util::write_all_to_stderr(stdout);
535                }
536                if !stderr.is_empty() {
537                    log::log!(log_level, "{} output on stderr:", self.tool.id());
538                    util::write_all_to_stderr(stderr);
539                }
540            }
541        }
542    }
543}
544
545impl ToolOutputPath {
546    /// Create a new `ToolOutputPath`.
547    ///
548    /// The `base_dir` is supposed to be the same as [`crate::runner::meta::Metadata::target_dir`].
549    pub fn new(
550        kind: ToolOutputPathKind,
551        tool: ValgrindTool,
552        baseline_kind: &BaselineKind,
553        base_dir: &Path,
554        module: &ModulePath,
555        name: &str,
556    ) -> Self {
557        let current = base_dir;
558        let module_path: PathBuf = module.to_string().split("::").collect();
559        let sanitized_name = sanitize_filename::sanitize_with_options(
560            name,
561            sanitize_filename::Options {
562                windows: false,
563                truncate: false,
564                replacement: "_",
565            },
566        );
567        let sanitized_name = truncate_str_utf8(&sanitized_name, 200);
568        Self {
569            kind,
570            tool,
571            baseline_kind: baseline_kind.clone(),
572            dir: current
573                .join(base_dir)
574                .join(module_path)
575                .join(sanitized_name),
576            name: sanitized_name.to_owned(),
577            modifiers: vec![],
578        }
579    }
580
581    /// Initialize and create the output directory and organize files
582    ///
583    /// This method moves the old output to `$TOOL_ID.*.out.old`
584    pub fn with_init(
585        kind: ToolOutputPathKind,
586        tool: ValgrindTool,
587        baseline_kind: &BaselineKind,
588        base_dir: &Path,
589        module: &str,
590        name: &str,
591    ) -> Result<Self> {
592        let output = Self::new(
593            kind,
594            tool,
595            baseline_kind,
596            base_dir,
597            &ModulePath::new(module),
598            name,
599        );
600        output.init()?;
601        Ok(output)
602    }
603
604    pub fn init(&self) -> Result<()> {
605        std::fs::create_dir_all(&self.dir).with_context(|| {
606            format!(
607                "Failed to create benchmark directory: '{}'",
608                self.dir.display()
609            )
610        })
611    }
612
613    pub fn clear(&self) -> Result<()> {
614        for entry in self.real_paths()? {
615            std::fs::remove_file(&entry).with_context(|| {
616                format!("Failed to remove benchmark file: '{}'", entry.display())
617            })?;
618        }
619        Ok(())
620    }
621
622    pub fn shift(&self) -> Result<()> {
623        match self.baseline_kind {
624            BaselineKind::Old => {
625                self.to_base_path().clear()?;
626                for entry in self.real_paths()? {
627                    let extension = entry.extension().expect("An extension should be present");
628                    let mut extension = extension.to_owned();
629                    extension.push(".old");
630                    let new_path = entry.with_extension(extension);
631                    std::fs::rename(&entry, &new_path).with_context(|| {
632                        format!(
633                            "Failed to move benchmark file from '{}' to '{}'",
634                            entry.display(),
635                            new_path.display()
636                        )
637                    })?;
638                }
639                Ok(())
640            }
641            BaselineKind::Name(_) => self.clear(),
642        }
643    }
644
645    pub fn exists(&self) -> bool {
646        self.real_paths().map_or(false, |p| !p.is_empty())
647    }
648
649    pub fn is_multiple(&self) -> bool {
650        self.real_paths().map_or(false, |p| p.len() > 1)
651    }
652
653    pub fn to_base_path(&self) -> Self {
654        Self {
655            kind: match (&self.kind, &self.baseline_kind) {
656                (ToolOutputPathKind::Out, BaselineKind::Old) => ToolOutputPathKind::OldOut,
657                (
658                    ToolOutputPathKind::Out | ToolOutputPathKind::Base(_),
659                    BaselineKind::Name(name),
660                ) => ToolOutputPathKind::Base(name.to_string()),
661                (ToolOutputPathKind::Log, BaselineKind::Old) => ToolOutputPathKind::OldLog,
662                (
663                    ToolOutputPathKind::Log | ToolOutputPathKind::BaseLog(_),
664                    BaselineKind::Name(name),
665                ) => ToolOutputPathKind::BaseLog(name.to_string()),
666                (kind, _) => kind.clone(),
667            },
668            tool: self.tool,
669            baseline_kind: self.baseline_kind.clone(),
670            name: self.name.clone(),
671            dir: self.dir.clone(),
672            modifiers: self.modifiers.clone(),
673        }
674    }
675
676    pub fn to_tool_output(&self, tool: ValgrindTool) -> Self {
677        Self {
678            tool,
679            kind: self.kind.clone(),
680            baseline_kind: self.baseline_kind.clone(),
681            name: self.name.clone(),
682            dir: self.dir.clone(),
683            modifiers: self.modifiers.clone(),
684        }
685    }
686
687    pub fn to_log_output(&self) -> Self {
688        Self {
689            kind: match &self.kind {
690                ToolOutputPathKind::Out | ToolOutputPathKind::OldOut => ToolOutputPathKind::Log,
691                ToolOutputPathKind::Base(name) => ToolOutputPathKind::BaseLog(name.clone()),
692                kind => kind.clone(),
693            },
694            tool: self.tool,
695            baseline_kind: self.baseline_kind.clone(),
696            name: self.name.clone(),
697            dir: self.dir.clone(),
698            modifiers: self.modifiers.clone(),
699        }
700    }
701
702    pub fn dump_log(&self, log_level: log::Level, writer: &mut impl Write) -> Result<()> {
703        if log_enabled!(log_level) {
704            for path in self.real_paths()? {
705                log::log!(
706                    log_level,
707                    "{} log output '{}':",
708                    self.tool.id(),
709                    path.display()
710                );
711
712                let file = File::open(&path).with_context(|| {
713                    format!(
714                        "Error opening {} output file '{}'",
715                        self.tool.id(),
716                        path.display()
717                    )
718                })?;
719
720                let mut reader = BufReader::new(file);
721                std::io::copy(&mut reader, writer)?;
722            }
723        }
724        Ok(())
725    }
726
727    /// This method can only be used to create the path passed to the tools
728    ///
729    /// The modifiers are extrapolated by the tools and won't match any real path name.
730    pub fn extension(&self) -> String {
731        match (&self.kind, self.modifiers.is_empty()) {
732            (ToolOutputPathKind::Out, true) => "out".to_owned(),
733            (ToolOutputPathKind::Out, false) => format!("out.{}", self.modifiers.join(".")),
734            (ToolOutputPathKind::Log, true) => "log".to_owned(),
735            (ToolOutputPathKind::Log, false) => format!("log.{}", self.modifiers.join(".")),
736            (ToolOutputPathKind::OldOut, true) => "out.old".to_owned(),
737            (ToolOutputPathKind::OldOut, false) => format!("out.old.{}", self.modifiers.join(".")),
738            (ToolOutputPathKind::OldLog, true) => "log.old".to_owned(),
739            (ToolOutputPathKind::OldLog, false) => format!("log.old.{}", self.modifiers.join(".")),
740            (ToolOutputPathKind::BaseLog(name), true) => {
741                format!("log.base@{name}")
742            }
743            (ToolOutputPathKind::BaseLog(name), false) => {
744                format!("log.base@{name}.{}", self.modifiers.join("."))
745            }
746            (ToolOutputPathKind::Base(name), true) => format!("out.base@{name}"),
747            (ToolOutputPathKind::Base(name), false) => {
748                format!("out.base@{name}.{}", self.modifiers.join("."))
749            }
750        }
751    }
752
753    pub fn with_modifiers<I, T>(&self, modifiers: T) -> Self
754    where
755        I: Into<String>,
756        T: IntoIterator<Item = I>,
757    {
758        Self {
759            kind: self.kind.clone(),
760            tool: self.tool,
761            baseline_kind: self.baseline_kind.clone(),
762            dir: self.dir.clone(),
763            name: self.name.clone(),
764            modifiers: modifiers.into_iter().map(Into::into).collect(),
765        }
766    }
767
768    // Return the unexpanded path usable as input for `--callgrind-out-file`, ...
769    //
770    // The path returned by this method does not necessarily have to exist and can include modifiers
771    // like `%p`. Use [`Self::real_paths`] to get the real and existing (possibly multiple) paths to
772    // the output files of the respective tool.
773    pub fn to_path(&self) -> PathBuf {
774        self.dir.join(format!(
775            "{}.{}.{}",
776            self.tool.id(),
777            self.name,
778            self.extension()
779        ))
780    }
781
782    /// Walk the benchmark directory (non-recursive)
783    pub fn walk_dir(&self) -> Result<impl Iterator<Item = DirEntry>> {
784        std::fs::read_dir(&self.dir)
785            .with_context(|| {
786                format!(
787                    "Failed opening benchmark directory: '{}'",
788                    self.dir.display()
789                )
790            })
791            .map(|i| i.into_iter().filter_map(Result::ok))
792    }
793
794    /// Strip the `<tool>.<name>` prefix from a `file_name`
795    pub fn strip_prefix<'a>(&self, file_name: &'a str) -> Option<&'a str> {
796        file_name.strip_prefix(format!("{}.{}", self.tool.id(), self.name).as_str())
797    }
798
799    /// Return the file name prefix as in `<tool>.<name>`
800    pub fn prefix(&self) -> String {
801        format!("{}.{}", self.tool.id(), self.name)
802    }
803
804    /// Return the `real` paths of a tool's output files
805    ///
806    /// A tool can have many output files so [`Self::to_path`] is not enough
807    #[allow(clippy::case_sensitive_file_extension_comparisons)]
808    pub fn real_paths(&self) -> Result<Vec<PathBuf>> {
809        let mut paths = vec![];
810        for entry in self.walk_dir()? {
811            let file_name = entry.file_name();
812            let file_name = file_name.to_string_lossy();
813
814            // Silently ignore all paths which don't follow this scheme, for example
815            // (`summary.json`)
816            if let Some(suffix) = self.strip_prefix(&file_name) {
817                let is_match = match &self.kind {
818                    ToolOutputPathKind::Out => suffix.ends_with(".out"),
819                    ToolOutputPathKind::Log => suffix.ends_with(".log"),
820                    ToolOutputPathKind::OldOut => suffix.ends_with(".out.old"),
821                    ToolOutputPathKind::OldLog => suffix.ends_with(".log.old"),
822                    ToolOutputPathKind::BaseLog(name) => {
823                        suffix.ends_with(format!(".log.base@{name}").as_str())
824                    }
825                    ToolOutputPathKind::Base(name) => {
826                        suffix.ends_with(format!(".out.base@{name}").as_str())
827                    }
828                };
829
830                if is_match {
831                    paths.push(entry.path());
832                }
833            }
834        }
835        Ok(paths)
836    }
837
838    pub fn real_paths_with_modifier(&self) -> Result<Vec<(PathBuf, Option<String>)>> {
839        let mut paths = vec![];
840        for entry in self.walk_dir()? {
841            let file_name = entry.file_name().to_string_lossy().to_string();
842
843            // Silently ignore all paths which don't follow this scheme, for example
844            // (`summary.json`)
845            if let Some(suffix) = self.strip_prefix(&file_name) {
846                let modifiers = match &self.kind {
847                    ToolOutputPathKind::Out => suffix.strip_suffix(".out"),
848                    ToolOutputPathKind::Log => suffix.strip_suffix(".log"),
849                    ToolOutputPathKind::OldOut => suffix.strip_suffix(".out.old"),
850                    ToolOutputPathKind::OldLog => suffix.strip_suffix(".log.old"),
851                    ToolOutputPathKind::BaseLog(name) => {
852                        suffix.strip_suffix(format!(".log.base@{name}").as_str())
853                    }
854                    ToolOutputPathKind::Base(name) => {
855                        suffix.strip_suffix(format!(".out.base@{name}").as_str())
856                    }
857                };
858
859                paths.push((
860                    entry.path(),
861                    modifiers.and_then(|s| (!s.is_empty()).then(|| s.to_owned())),
862                ));
863            }
864        }
865        Ok(paths)
866    }
867
868    /// Sanitize callgrind output file names
869    ///
870    /// This method will remove empty files which are occasionally produced by callgrind and only
871    /// cause problems in the parser. The files are renamed from the callgrind file naming scheme to
872    /// ours which is easier to handle.
873    ///
874    /// The information about pids, parts and threads is obtained by parsing the header from the
875    /// callgrind output files instead of relying on the sometimes flaky file names produced by
876    /// `callgrind`. The header is around 10-20 lines, so this method should be still sufficiently
877    /// fast. Additionally, `callgrind` might change the naming scheme of its files, so using the
878    /// headers makes us more independent of a specific valgrind/callgrind version.
879    pub fn sanitize_callgrind(&self) -> Result<()> {
880        // path, part
881        type Grouped = (PathBuf, Option<u64>);
882        // base (i.e. base@default) => pid => thread => vec: path, part
883        type Group =
884            HashMap<Option<String>, HashMap<Option<i32>, HashMap<Option<usize>, Vec<Grouped>>>>;
885
886        // To figure out if there are multiple pids/parts/threads present, it's necessary to group
887        // the files in this map. The order doesn't matter since we only rename the original file
888        // names, which doesn't need to follow a specific order.
889        //
890        // At first, we group by (out|log), then base, then pid and then by part in different
891        // hashmaps. The threads are grouped in a vector.
892        let mut groups: HashMap<String, Group> = HashMap::new();
893
894        for entry in self.walk_dir()? {
895            let file_name = entry.file_name();
896            let file_name = file_name.to_string_lossy();
897
898            let Some(haystack) = self.strip_prefix(&file_name) else {
899                continue;
900            };
901
902            if let Some(caps) = CALLGRIND_ORIG_FILENAME_RE.captures(haystack) {
903                // Callgrind sometimes creates empty files for no reason. We clean them
904                // up here
905                if entry.metadata()?.size() == 0 {
906                    std::fs::remove_file(entry.path())?;
907                    continue;
908                }
909
910                // We don't sanitize old files. It's not needed if the new files are always
911                // sanitized. However, we do sanitize `base@<name>` file names.
912                let base = if let Some(base) = caps.name("base") {
913                    if base.as_str() == ".old" {
914                        continue;
915                    }
916
917                    Some(base.as_str().to_owned())
918                } else {
919                    None
920                };
921
922                let out_type = caps
923                    .name("type")
924                    .expect("A out|log type should be present")
925                    .as_str();
926
927                if out_type == ".out" {
928                    let properties = parse_header(
929                        &mut BufReader::new(File::open(entry.path())?)
930                            .lines()
931                            .map(Result::unwrap),
932                    )?;
933                    if let Some(bases) = groups.get_mut(out_type) {
934                        if let Some(pids) = bases.get_mut(&base) {
935                            if let Some(threads) = pids.get_mut(&properties.pid) {
936                                if let Some(parts) = threads.get_mut(&properties.thread) {
937                                    parts.push((entry.path(), properties.part));
938                                } else {
939                                    threads.insert(
940                                        properties.thread,
941                                        vec![(entry.path(), properties.part)],
942                                    );
943                                }
944                            } else {
945                                pids.insert(
946                                    properties.pid,
947                                    HashMap::from([(
948                                        properties.thread,
949                                        vec![(entry.path(), properties.part)],
950                                    )]),
951                                );
952                            }
953                        } else {
954                            bases.insert(
955                                base.clone(),
956                                HashMap::from([(
957                                    properties.pid,
958                                    HashMap::from([(
959                                        properties.thread,
960                                        vec![(entry.path(), properties.part)],
961                                    )]),
962                                )]),
963                            );
964                        }
965                    } else {
966                        groups.insert(
967                            out_type.to_owned(),
968                            HashMap::from([(
969                                base.clone(),
970                                HashMap::from([(
971                                    properties.pid,
972                                    HashMap::from([(
973                                        properties.thread,
974                                        vec![(entry.path(), properties.part)],
975                                    )]),
976                                )]),
977                            )]),
978                        );
979                    }
980                } else {
981                    let pid = caps.name("pid").map(|m| {
982                        m.as_str()[2..]
983                            .parse::<i32>()
984                            .expect("The pid from the match should be number")
985                    });
986
987                    // The log files don't expose any information about parts or threads, so
988                    // these are grouped under the `None` key
989                    if let Some(bases) = groups.get_mut(out_type) {
990                        if let Some(pids) = bases.get_mut(&base) {
991                            if let Some(threads) = pids.get_mut(&pid) {
992                                if let Some(parts) = threads.get_mut(&None) {
993                                    parts.push((entry.path(), None));
994                                } else {
995                                    threads.insert(None, vec![(entry.path(), None)]);
996                                }
997                            } else {
998                                pids.insert(
999                                    pid,
1000                                    HashMap::from([(None, vec![(entry.path(), None)])]),
1001                                );
1002                            }
1003                        } else {
1004                            bases.insert(
1005                                base.clone(),
1006                                HashMap::from([(
1007                                    pid,
1008                                    HashMap::from([(None, vec![(entry.path(), None)])]),
1009                                )]),
1010                            );
1011                        }
1012                    } else {
1013                        groups.insert(
1014                            out_type.to_owned(),
1015                            HashMap::from([(
1016                                base.clone(),
1017                                HashMap::from([(
1018                                    pid,
1019                                    HashMap::from([(None, vec![(entry.path(), None)])]),
1020                                )]),
1021                            )]),
1022                        );
1023                    }
1024                }
1025            }
1026        }
1027
1028        for (out_type, types) in groups {
1029            for (base, bases) in types {
1030                let multiple_pids = bases.len() > 1;
1031
1032                for (pid, threads) in bases {
1033                    let multiple_threads = threads.len() > 1;
1034
1035                    for (thread, parts) in &threads {
1036                        let multiple_parts = parts.len() > 1;
1037
1038                        for (orig_path, part) in parts {
1039                            let mut new_file_name = self.prefix();
1040
1041                            if multiple_pids {
1042                                if let Some(pid) = pid {
1043                                    write!(new_file_name, ".{pid}").unwrap();
1044                                }
1045                            }
1046
1047                            if multiple_threads {
1048                                if let Some(thread) = thread {
1049                                    let width = threads.len().ilog10() as usize + 1;
1050                                    write!(new_file_name, ".t{thread:0width$}").unwrap();
1051                                }
1052
1053                                if !multiple_parts {
1054                                    if let Some(part) = part {
1055                                        let width = parts.len().ilog10() as usize + 1;
1056                                        write!(new_file_name, ".p{part:0width$}").unwrap();
1057                                    }
1058                                }
1059                            }
1060
1061                            if multiple_parts {
1062                                if !multiple_threads {
1063                                    if let Some(thread) = thread {
1064                                        let width = threads.len().ilog10() as usize + 1;
1065                                        write!(new_file_name, ".t{thread:0width$}").unwrap();
1066                                    }
1067                                }
1068
1069                                if let Some(part) = part {
1070                                    let width = parts.len().ilog10() as usize + 1;
1071                                    write!(new_file_name, ".p{part:0width$}").unwrap();
1072                                }
1073                            }
1074
1075                            new_file_name.push_str(&out_type);
1076                            if let Some(base) = &base {
1077                                new_file_name.push_str(base);
1078                            }
1079
1080                            let from = orig_path;
1081                            let to = from.with_file_name(new_file_name);
1082
1083                            std::fs::rename(from, to)?;
1084                        }
1085                    }
1086                }
1087            }
1088        }
1089
1090        Ok(())
1091    }
1092
1093    // Sanitize bbv file names
1094    //
1095    // The original output files of bb have a `.<number>` suffix if there are multiple threads. We
1096    // need the threads as `t<number>` in the modifier part of the final file names.
1097    //
1098    // For example: (orig -> sanitized)
1099    //
1100    // If there are multiple threads, the bb output file name doesn't include the first thread:
1101    //
1102    // `exp-bbv.bench_thread_in_subprocess.548365.bb.out` ->
1103    // `exp-bbv.bench_thread_in_subprocess.548365.t1.bb.out`
1104    //
1105    // `exp-bbv.bench_thread_in_subprocess.548365.bb.out.2` ->
1106    // `exp-bbv.bench_thread_in_subprocess.548365.t2.bb.out`
1107    #[allow(clippy::case_sensitive_file_extension_comparisons)]
1108    pub fn sanitize_bbv(&self) -> Result<()> {
1109        // path, thread,
1110        type Grouped = (PathBuf, String);
1111        // key: bbv_type => key: pid
1112        type Group =
1113            HashMap<Option<String>, HashMap<Option<String>, HashMap<Option<String>, Vec<Grouped>>>>;
1114
1115        // key: .(out|log)
1116        let mut groups: HashMap<String, Group> = HashMap::new();
1117        for entry in self.walk_dir()? {
1118            let file_name = entry.file_name();
1119            let file_name = file_name.to_string_lossy();
1120
1121            let Some(haystack) = self.strip_prefix(&file_name) else {
1122                continue;
1123            };
1124
1125            if let Some(caps) = BBV_ORIG_FILENAME_RE.captures(haystack) {
1126                if entry.metadata()?.size() == 0 {
1127                    std::fs::remove_file(entry.path())?;
1128                    continue;
1129                }
1130
1131                // Don't sanitize old files.
1132                let base = if let Some(base) = caps.name("base") {
1133                    if base.as_str() == ".old" {
1134                        continue;
1135                    }
1136
1137                    Some(base.as_str().to_owned())
1138                } else {
1139                    None
1140                };
1141
1142                let out_type = caps.name("type").unwrap().as_str();
1143                let bbv_type = caps.name("bbv_type").map(|m| m.as_str().to_owned());
1144                let pid = caps.name("pid").map(|p| format!(".{}", &p.as_str()[2..]));
1145
1146                let thread = caps
1147                    .name("thread")
1148                    .map_or_else(|| ".1".to_owned(), |t| t.as_str().to_owned());
1149
1150                if let Some(bases) = groups.get_mut(out_type) {
1151                    if let Some(bbv_types) = bases.get_mut(&base) {
1152                        if let Some(pids) = bbv_types.get_mut(&bbv_type) {
1153                            if let Some(threads) = pids.get_mut(&pid) {
1154                                threads.push((entry.path(), thread));
1155                            } else {
1156                                pids.insert(pid, vec![(entry.path(), thread)]);
1157                            };
1158                        } else {
1159                            bbv_types.insert(
1160                                bbv_type.clone(),
1161                                HashMap::from([(pid, vec![(entry.path(), thread)])]),
1162                            );
1163                        }
1164                    } else {
1165                        bases.insert(
1166                            base.clone(),
1167                            HashMap::from([(
1168                                bbv_type.clone(),
1169                                HashMap::from([(pid, vec![(entry.path(), thread)])]),
1170                            )]),
1171                        );
1172                    }
1173                } else {
1174                    groups.insert(
1175                        out_type.to_owned(),
1176                        HashMap::from([(
1177                            base.clone(),
1178                            HashMap::from([(
1179                                bbv_type.clone(),
1180                                HashMap::from([(pid, vec![(entry.path(), thread)])]),
1181                            )]),
1182                        )]),
1183                    );
1184                }
1185            }
1186        }
1187
1188        for (out_type, bases) in groups {
1189            for (base, bbv_types) in bases {
1190                for (bbv_type, pids) in &bbv_types {
1191                    let multiple_pids = pids.len() > 1;
1192
1193                    for (pid, threads) in pids {
1194                        let multiple_threads = threads.len() > 1;
1195
1196                        for (orig_path, thread) in threads {
1197                            let mut new_file_name = self.prefix();
1198
1199                            if multiple_pids {
1200                                if let Some(pid) = pid.as_ref() {
1201                                    write!(new_file_name, "{pid}").unwrap();
1202                                }
1203                            }
1204
1205                            if multiple_threads
1206                                && bbv_type.as_ref().map_or(false, |b| b.starts_with(".bb"))
1207                            {
1208                                let width = threads.len().ilog10() as usize + 1;
1209
1210                                let thread = thread[1..]
1211                                    .parse::<usize>()
1212                                    .expect("The thread from the regex should be a number");
1213
1214                                write!(new_file_name, ".t{thread:0width$}").unwrap();
1215                            }
1216
1217                            if let Some(bbv_type) = &bbv_type {
1218                                new_file_name.push_str(bbv_type);
1219                            }
1220
1221                            new_file_name.push_str(&out_type);
1222
1223                            if let Some(base) = &base {
1224                                new_file_name.push_str(base);
1225                            }
1226
1227                            let from = orig_path;
1228                            let to = from.with_file_name(new_file_name);
1229
1230                            std::fs::rename(from, to)?;
1231                        }
1232                    }
1233                }
1234            }
1235        }
1236
1237        Ok(())
1238    }
1239
1240    /// Sanitize file names of all tools if not sanitized by a more specific method
1241    ///
1242    /// The pids are removed from the file name if there was only a single process (pid).
1243    /// Additionally, we check for empty files and remove them.
1244    pub fn sanitize_generic(&self) -> Result<()> {
1245        // key: base => vec: path, pid
1246        type Group = HashMap<Option<String>, Vec<(PathBuf, Option<String>)>>;
1247
1248        // key: .(out|log)
1249        let mut groups: HashMap<String, Group> = HashMap::new();
1250        for entry in self.walk_dir()? {
1251            let file_name = entry.file_name();
1252            let file_name = file_name.to_string_lossy();
1253
1254            let Some(haystack) = self.strip_prefix(&file_name) else {
1255                continue;
1256            };
1257
1258            if let Some(caps) = GENERIC_ORIG_FILENAME_RE.captures(haystack) {
1259                if entry.metadata()?.size() == 0 {
1260                    std::fs::remove_file(entry.path())?;
1261                    continue;
1262                }
1263
1264                // Don't sanitize old files.
1265                let base = if let Some(base) = caps.name("base") {
1266                    if base.as_str() == ".old" {
1267                        continue;
1268                    }
1269
1270                    Some(base.as_str().to_owned())
1271                } else {
1272                    None
1273                };
1274
1275                let out_type = caps.name("type").unwrap().as_str();
1276                let pid = caps.name("pid").map(|p| format!(".{}", &p.as_str()[2..]));
1277
1278                if let Some(bases) = groups.get_mut(out_type) {
1279                    if let Some(pids) = bases.get_mut(&base) {
1280                        pids.push((entry.path(), pid));
1281                    } else {
1282                        bases.insert(base, vec![(entry.path(), pid)]);
1283                    }
1284                } else {
1285                    groups.insert(
1286                        out_type.to_owned(),
1287                        HashMap::from([(base, vec![(entry.path(), pid)])]),
1288                    );
1289                }
1290            }
1291        }
1292
1293        for (out_type, bases) in groups {
1294            for (base, pids) in bases {
1295                let multiple_pids = pids.len() > 1;
1296                for (orig_path, pid) in pids {
1297                    let mut new_file_name = self.prefix();
1298
1299                    if multiple_pids {
1300                        if let Some(pid) = pid.as_ref() {
1301                            write!(new_file_name, "{pid}").unwrap();
1302                        }
1303                    }
1304
1305                    new_file_name.push_str(&out_type);
1306
1307                    if let Some(base) = &base {
1308                        new_file_name.push_str(base);
1309                    }
1310
1311                    let from = orig_path;
1312                    let to = from.with_file_name(new_file_name);
1313
1314                    std::fs::rename(from, to)?;
1315                }
1316            }
1317        }
1318
1319        Ok(())
1320    }
1321
1322    /// Sanitize file names for a specific tool
1323    ///
1324    /// Empty files are cleaned up. For more details on a specific tool see the respective
1325    /// sanitize_<tool> method.
1326    pub fn sanitize(&self) -> Result<()> {
1327        match self.tool {
1328            ValgrindTool::Callgrind => self.sanitize_callgrind()?,
1329            ValgrindTool::BBV => self.sanitize_bbv()?,
1330            _ => self.sanitize_generic()?,
1331        };
1332
1333        Ok(())
1334    }
1335}
1336
1337impl Display for ToolOutputPath {
1338    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1339        f.write_fmt(format_args!("{}", self.to_path().display()))
1340    }
1341}
1342
1343impl ValgrindTool {
1344    /// Return the id used by the `valgrind --tool` option
1345    pub fn id(&self) -> String {
1346        match self {
1347            ValgrindTool::DHAT => "dhat".to_owned(),
1348            ValgrindTool::Callgrind => "callgrind".to_owned(),
1349            ValgrindTool::Memcheck => "memcheck".to_owned(),
1350            ValgrindTool::Helgrind => "helgrind".to_owned(),
1351            ValgrindTool::DRD => "drd".to_owned(),
1352            ValgrindTool::Massif => "massif".to_owned(),
1353            ValgrindTool::BBV => "exp-bbv".to_owned(),
1354        }
1355    }
1356
1357    pub fn has_output_file(&self) -> bool {
1358        matches!(
1359            self,
1360            ValgrindTool::Callgrind | ValgrindTool::DHAT | ValgrindTool::BBV | ValgrindTool::Massif
1361        )
1362    }
1363}
1364
1365impl Display for ValgrindTool {
1366    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1367        f.write_str(&self.id())
1368    }
1369}
1370
1371impl From<api::ValgrindTool> for ValgrindTool {
1372    fn from(value: api::ValgrindTool) -> Self {
1373        match value {
1374            api::ValgrindTool::Memcheck => ValgrindTool::Memcheck,
1375            api::ValgrindTool::Helgrind => ValgrindTool::Helgrind,
1376            api::ValgrindTool::DRD => ValgrindTool::DRD,
1377            api::ValgrindTool::Massif => ValgrindTool::Massif,
1378            api::ValgrindTool::DHAT => ValgrindTool::DHAT,
1379            api::ValgrindTool::BBV => ValgrindTool::BBV,
1380        }
1381    }
1382}
1383
1384impl TryFrom<&str> for ValgrindTool {
1385    type Error = anyhow::Error;
1386
1387    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
1388        match value {
1389            "dhat" => Ok(ValgrindTool::DHAT),
1390            "callgrind" => Ok(ValgrindTool::Callgrind),
1391            "memcheck" => Ok(ValgrindTool::Memcheck),
1392            "helgrind" => Ok(ValgrindTool::Helgrind),
1393            "drd" => Ok(ValgrindTool::DRD),
1394            "massif" => Ok(ValgrindTool::Massif),
1395            "exp-bbv" => Ok(ValgrindTool::BBV),
1396            v => Err(anyhow!("Unknown tool '{}'", v)),
1397        }
1398    }
1399}
1400
1401pub fn check_exit(
1402    tool: ValgrindTool,
1403    executable: &Path,
1404    output: Option<Output>,
1405    status: ExitStatus,
1406    output_path: &ToolOutputPath,
1407    exit_with: Option<&ExitWith>,
1408) -> Result<Option<Output>> {
1409    let Some(status_code) = status.code() else {
1410        return Err(
1411            Error::ProcessError((tool.id(), output, status, Some(output_path.clone()))).into(),
1412        );
1413    };
1414
1415    match (status_code, exit_with) {
1416        (0i32, None | Some(ExitWith::Code(0i32) | ExitWith::Success)) => Ok(output),
1417        (0i32, Some(ExitWith::Code(code))) => {
1418            error!(
1419                "{}: Expected '{}' to exit with '{}' but it succeeded",
1420                tool.id(),
1421                executable.display(),
1422                code
1423            );
1424            Err(Error::ProcessError((tool.id(), output, status, Some(output_path.clone()))).into())
1425        }
1426        (0i32, Some(ExitWith::Failure)) => {
1427            error!(
1428                "{}: Expected '{}' to fail but it succeeded",
1429                tool.id(),
1430                executable.display(),
1431            );
1432            Err(Error::ProcessError((tool.id(), output, status, Some(output_path.clone()))).into())
1433        }
1434        (_, Some(ExitWith::Failure)) => Ok(output),
1435        (code, Some(ExitWith::Success)) => {
1436            error!(
1437                "{}: Expected '{}' to succeed but it terminated with '{}'",
1438                tool.id(),
1439                executable.display(),
1440                code
1441            );
1442            Err(Error::ProcessError((tool.id(), output, status, Some(output_path.clone()))).into())
1443        }
1444        (actual_code, Some(ExitWith::Code(expected_code))) if actual_code == *expected_code => {
1445            Ok(output)
1446        }
1447        (actual_code, Some(ExitWith::Code(expected_code))) => {
1448            error!(
1449                "{}: Expected '{}' to exit with '{}' but it terminated with '{}'",
1450                tool.id(),
1451                executable.display(),
1452                expected_code,
1453                actual_code
1454            );
1455            Err(Error::ProcessError((tool.id(), output, status, Some(output_path.clone()))).into())
1456        }
1457        _ => {
1458            Err(Error::ProcessError((tool.id(), output, status, Some(output_path.clone()))).into())
1459        }
1460    }
1461}
1462
1463#[cfg(test)]
1464mod tests {
1465
1466    use rstest::rstest;
1467
1468    use super::*;
1469
1470    #[rstest]
1471    #[case::out(".out")]
1472    #[case::out_with_pid(".out.#1234")]
1473    #[case::out_with_part(".out.1")]
1474    #[case::out_with_thread(".out-01")]
1475    #[case::out_with_part_and_thread(".out.1-01")]
1476    #[case::out_base(".out.base@default")]
1477    #[case::out_base_with_pid(".out.base@default.#1234")]
1478    #[case::out_base_with_part(".out.base@default.1")]
1479    #[case::out_base_with_thread(".out.base@default-01")]
1480    #[case::out_base_with_part_and_thread(".out.base@default.1-01")]
1481    #[case::log(".log")]
1482    #[case::log_with_pid(".log.#1234")]
1483    #[case::log_base(".log.base@default")]
1484    #[case::log_base_with_pid(".log.base@default.#1234")]
1485    fn test_callgrind_filename_regex(#[case] haystack: &str) {
1486        assert!(CALLGRIND_ORIG_FILENAME_RE.is_match(haystack));
1487    }
1488
1489    #[rstest]
1490    #[case::bb_out(".out.bb")]
1491    #[case::bb_out_with_pid(".out.bb.#1234")]
1492    #[case::bb_out_with_pid_and_thread(".out.bb.#1234.1")]
1493    #[case::bb_out_with_thread(".out.bb.1")]
1494    #[case::pc_out(".out.pc")]
1495    #[case::log(".log")]
1496    #[case::log_with_pid(".log.#1234")]
1497    fn test_bbv_filename_regex(#[case] haystack: &str) {
1498        assert!(BBV_ORIG_FILENAME_RE.is_match(haystack));
1499    }
1500}