iai_callgrind_runner/runner/tool/
path.rs

1//! The module containing the [`ToolOutputPath`] and other related elements
2
3use std::collections::HashMap;
4use std::fmt::{Display, Write as FmtWrite};
5use std::fs::{DirEntry, File};
6use std::io::{BufRead, BufReader, Write};
7use std::os::unix::fs::MetadataExt;
8use std::path::{Path, PathBuf};
9
10use anyhow::{Context, Result};
11use lazy_static::lazy_static;
12use log::log_enabled;
13use regex::Regex;
14
15use crate::api::ValgrindTool;
16use crate::runner::callgrind::parser::parse_header;
17use crate::runner::common::ModulePath;
18use crate::runner::summary::BaselineKind;
19use crate::util::truncate_str_utf8;
20
21lazy_static! {
22    // This regex matches the original file name without the prefix as it is created by callgrind.
23    // The baseline <name> (base@<name>) can only consist of ascii and underscore characters.
24    // Flamegraph files are ignored by this regex
25    //
26    // Note callgrind doesn't support xtree, xleak files
27    static ref CALLGRIND_ORIG_FILENAME_RE: Regex = Regex::new(
28        "^(?<type>[.](out|log))(?<base>[.](old|base@[^.-]+))?(?<pid>[.][#][0-9]+)?(?<part>[.][0-9]+)?(?<thread>-[0-9]+)?$"
29    )
30    .expect("Regex should compile");
31
32    /// This regex matches the original file name without the prefix as it is created by bbv
33    ///
34    /// Note bbv doesn't support xtree, xleak files
35    static ref BBV_ORIG_FILENAME_RE: Regex = Regex::new(
36        "^(?<type>[.](?:out|log))(?<base>[.](old|base@[^.]+))?(?<bbv_type>[.](?:bb|pc))?(?<pid>[.][#][0-9]+)?(?<thread>[.][0-9]+)?$"
37    )
38    .expect("Regex should compile");
39
40    /// This regex matches the original file name without the prefix as it is created by all tools
41    /// other than callgrind and bbv.
42    static ref GENERIC_ORIG_FILENAME_RE: Regex = Regex::new(
43        "^(?<type>[.](?:out|log|xtree|xleak))(?<base>[.](old|base@[^.]+))?(?<pid>[.][#][0-9]+)?$"
44    )
45    .expect("Regex should compile");
46
47    static ref REAL_FILENAME_RE: Regex = Regex::new(
48        "^(?:[.](?<pid>[0-9]+))?(?:[.]t(?<tid>[0-9]+))?(?:[.]p(?<part>[0-9]+))?(?:[.](?<bbv>bb|pc))?(?:[.](?<type>out|log|xtree|xleak))(?:[.](?<base>old|base@[^.]+))?$"
49    )
50    .expect("Regex should compile");
51}
52
53/// The different output path kinds
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum ToolOutputPathKind {
56    /// The output path for `*.out` files
57    Out,
58    /// The output path for `*.out.old` files
59    OldOut,
60    /// The output path for baseline `out` files
61    BaseOut(String),
62    /// The output path for `*.log` files
63    Log,
64    /// The output path for `*.log.old` files
65    OldLog,
66    /// The output path for baseline `log` files
67    BaseLog(String),
68    /// The output path for `*.xtree` files
69    Xtree,
70    /// The output path for `*.xtree.old` files
71    OldXtree,
72    /// The output for baseline `xtree` files
73    BaseXtree(String),
74    /// The output path for `*.xleak` files
75    Xleak,
76    /// The output path for `*.xleak.old` files
77    OldXleak,
78    /// The output for baseline `xleak` files
79    BaseXleak(String),
80}
81
82/// The tool specific output path(s)
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ToolOutputPath {
85    /// The [`BaselineKind`]
86    pub baseline_kind: BaselineKind,
87    /// The final directory of all the output files
88    pub dir: PathBuf,
89    /// The [`ToolOutputPathKind`]
90    pub kind: ToolOutputPathKind,
91    /// The modifiers which are prepended to the extension
92    pub modifiers: Vec<String>,
93    /// The name of this output path
94    pub name: String,
95    /// The tool
96    pub tool: ValgrindTool,
97}
98
99impl ToolOutputPath {
100    /// Create a new `ToolOutputPath`.
101    ///
102    /// The `base_dir` is supposed to be the same as [`crate::runner::meta::Metadata::target_dir`].
103    /// The `name` is supposed to be the name of the benchmark function. If a benchmark id is
104    /// present join both with a dot as separator to get the final `name`.
105    pub fn new(
106        kind: ToolOutputPathKind,
107        tool: ValgrindTool,
108        baseline_kind: &BaselineKind,
109        base_dir: &Path,
110        module: &ModulePath,
111        name: &str,
112    ) -> Self {
113        let current = base_dir;
114        let module_path: PathBuf = module.to_string().split("::").collect();
115        let sanitized_name = sanitize_filename::sanitize_with_options(
116            name,
117            sanitize_filename::Options {
118                windows: false,
119                truncate: false,
120                replacement: "_",
121            },
122        );
123        let sanitized_name = truncate_str_utf8(&sanitized_name, 200);
124        Self {
125            kind,
126            tool,
127            baseline_kind: baseline_kind.clone(),
128            dir: current
129                .join(base_dir)
130                .join(module_path)
131                .join(sanitized_name),
132            name: sanitized_name.to_owned(),
133            modifiers: vec![],
134        }
135    }
136
137    /// Initialize and create the output directory and organize files
138    ///
139    /// This method moves the old output to `$TOOL_ID.*.out.old`
140    pub fn with_init(
141        kind: ToolOutputPathKind,
142        tool: ValgrindTool,
143        baseline_kind: &BaselineKind,
144        base_dir: &Path,
145        module: &str,
146        name: &str,
147    ) -> Result<Self> {
148        let output = Self::new(
149            kind,
150            tool,
151            baseline_kind,
152            base_dir,
153            &ModulePath::new(module),
154            name,
155        );
156        output.init()?;
157        Ok(output)
158    }
159
160    /// Initialize the directory in which the files are stored
161    pub fn init(&self) -> Result<()> {
162        std::fs::create_dir_all(&self.dir).with_context(|| {
163            format!(
164                "Failed to create benchmark directory: '{}'",
165                self.dir.display()
166            )
167        })
168    }
169
170    /// Remove the files of this output path
171    pub fn clear(&self) -> Result<()> {
172        for entry in self.real_paths()? {
173            std::fs::remove_file(&entry).with_context(|| {
174                format!("Failed to remove benchmark file: '{}'", entry.display())
175            })?;
176        }
177        Ok(())
178    }
179
180    /// Remove the old or base files and rename the present files to "old" files
181    pub fn shift(&self) -> Result<()> {
182        match self.baseline_kind {
183            BaselineKind::Old => {
184                self.to_base_path().clear()?;
185                for entry in self.real_paths()? {
186                    let extension = entry.extension().expect("An extension should be present");
187                    let mut extension = extension.to_owned();
188                    extension.push(".old");
189                    let new_path = entry.with_extension(extension);
190                    std::fs::rename(&entry, &new_path).with_context(|| {
191                        format!(
192                            "Failed to move benchmark file from '{}' to '{}'",
193                            entry.display(),
194                            new_path.display()
195                        )
196                    })?;
197                }
198                Ok(())
199            }
200            BaselineKind::Name(_) => self.clear(),
201        }
202    }
203
204    /// Return true if a real file of this output path exists
205    pub fn exists(&self) -> bool {
206        self.real_paths().is_ok_and(|p| !p.is_empty())
207    }
208
209    /// Return true if there are multiple real files of this output path
210    pub fn is_multiple(&self) -> bool {
211        self.real_paths().is_ok_and(|p| p.len() > 1)
212    }
213
214    /// Convert this output path to a base output path
215    #[must_use]
216    pub fn to_base_path(&self) -> Self {
217        Self {
218            kind: match (&self.kind, &self.baseline_kind) {
219                (ToolOutputPathKind::Out, BaselineKind::Old) => ToolOutputPathKind::OldOut,
220                (
221                    ToolOutputPathKind::Out | ToolOutputPathKind::BaseOut(_),
222                    BaselineKind::Name(name),
223                ) => ToolOutputPathKind::BaseOut(name.to_string()),
224                (ToolOutputPathKind::Log, BaselineKind::Old) => ToolOutputPathKind::OldLog,
225                (
226                    ToolOutputPathKind::Log | ToolOutputPathKind::BaseLog(_),
227                    BaselineKind::Name(name),
228                ) => ToolOutputPathKind::BaseLog(name.to_string()),
229                (ToolOutputPathKind::Xtree, BaselineKind::Old) => ToolOutputPathKind::OldXtree,
230                (
231                    ToolOutputPathKind::Xtree | ToolOutputPathKind::BaseXtree(_),
232                    BaselineKind::Name(name),
233                ) => ToolOutputPathKind::BaseXtree(name.to_string()),
234                (ToolOutputPathKind::Xleak, BaselineKind::Old) => ToolOutputPathKind::OldXleak,
235                (
236                    ToolOutputPathKind::Xleak | ToolOutputPathKind::BaseXleak(_),
237                    BaselineKind::Name(name),
238                ) => ToolOutputPathKind::BaseXleak(name.to_string()),
239                (kind, _) => kind.clone(),
240            },
241            tool: self.tool,
242            baseline_kind: self.baseline_kind.clone(),
243            name: self.name.clone(),
244            dir: self.dir.clone(),
245            modifiers: self.modifiers.clone(),
246        }
247    }
248
249    /// Convert this tool output path to the output of another tool output path
250    ///
251    /// A tool with no `*.out` file is log-file based. If the other tool is a out-file based tool
252    /// the [`ToolOutputPathKind`] will be converted and vice-versa. The "old" (base) type (a tool
253    /// output converted with [`ToolOutputPath::to_base_path`]) will be converted to a new
254    /// `ToolOutputPath`.
255    #[must_use]
256    pub fn to_tool_output(&self, tool: ValgrindTool) -> Self {
257        let kind = if tool.has_output_file() {
258            match &self.kind {
259                ToolOutputPathKind::Log
260                | ToolOutputPathKind::OldLog
261                | ToolOutputPathKind::Xtree
262                | ToolOutputPathKind::OldXtree
263                | ToolOutputPathKind::Xleak
264                | ToolOutputPathKind::OldXleak => ToolOutputPathKind::Out,
265                ToolOutputPathKind::BaseLog(name)
266                | ToolOutputPathKind::BaseXtree(name)
267                | ToolOutputPathKind::BaseXleak(name) => ToolOutputPathKind::BaseOut(name.clone()),
268                kind => kind.clone(),
269            }
270        } else {
271            match &self.kind {
272                ToolOutputPathKind::Out
273                | ToolOutputPathKind::OldOut
274                | ToolOutputPathKind::Xtree
275                | ToolOutputPathKind::OldXtree
276                | ToolOutputPathKind::Xleak
277                | ToolOutputPathKind::OldXleak => ToolOutputPathKind::Log,
278                ToolOutputPathKind::BaseOut(name)
279                | ToolOutputPathKind::BaseXtree(name)
280                | ToolOutputPathKind::BaseXleak(name) => ToolOutputPathKind::BaseLog(name.clone()),
281                kind => kind.clone(),
282            }
283        };
284        Self {
285            tool,
286            kind,
287            baseline_kind: self.baseline_kind.clone(),
288            name: self.name.clone(),
289            dir: self.dir.clone(),
290            modifiers: self.modifiers.clone(),
291        }
292    }
293
294    /// Convert this tool output to the according log output
295    ///
296    /// All tools have a log output even the ones which are out-file based.
297    #[must_use]
298    pub fn to_log_output(&self) -> Self {
299        Self {
300            kind: match &self.kind {
301                ToolOutputPathKind::Out
302                | ToolOutputPathKind::OldOut
303                | ToolOutputPathKind::Xleak
304                | ToolOutputPathKind::OldXleak
305                | ToolOutputPathKind::Xtree
306                | ToolOutputPathKind::OldXtree => ToolOutputPathKind::Log,
307                ToolOutputPathKind::BaseOut(name)
308                | ToolOutputPathKind::BaseXtree(name)
309                | ToolOutputPathKind::BaseXleak(name) => ToolOutputPathKind::BaseLog(name.clone()),
310                kind => kind.clone(),
311            },
312            tool: self.tool,
313            baseline_kind: self.baseline_kind.clone(),
314            name: self.name.clone(),
315            dir: self.dir.clone(),
316            modifiers: self.modifiers.clone(),
317        }
318    }
319
320    /// If possible, convert this tool output to the according xtree output
321    ///
322    /// Not all tools support xtree output files
323    #[must_use]
324    pub fn to_xtree_output(&self) -> Option<Self> {
325        self.tool.has_xtree_file().then(|| Self {
326            kind: match &self.kind {
327                ToolOutputPathKind::Out
328                | ToolOutputPathKind::OldOut
329                | ToolOutputPathKind::Xleak
330                | ToolOutputPathKind::OldXleak
331                | ToolOutputPathKind::Log
332                | ToolOutputPathKind::OldLog => ToolOutputPathKind::Xtree,
333                ToolOutputPathKind::BaseOut(name)
334                | ToolOutputPathKind::BaseLog(name)
335                | ToolOutputPathKind::BaseXleak(name) => {
336                    ToolOutputPathKind::BaseXtree(name.clone())
337                }
338                kind => kind.clone(),
339            },
340            tool: self.tool,
341            baseline_kind: self.baseline_kind.clone(),
342            name: self.name.clone(),
343            dir: self.dir.clone(),
344            modifiers: self.modifiers.clone(),
345        })
346    }
347
348    /// If possible, convert this tool output to the according xleak output
349    ///
350    /// Not all tools support xleak output files
351    #[must_use]
352    pub fn to_xleak_output(&self) -> Option<Self> {
353        self.tool.has_xleak_file().then(|| Self {
354            kind: match &self.kind {
355                ToolOutputPathKind::Out
356                | ToolOutputPathKind::OldOut
357                | ToolOutputPathKind::Xtree
358                | ToolOutputPathKind::OldXtree
359                | ToolOutputPathKind::Log
360                | ToolOutputPathKind::OldLog => ToolOutputPathKind::Xleak,
361                ToolOutputPathKind::BaseOut(name)
362                | ToolOutputPathKind::BaseLog(name)
363                | ToolOutputPathKind::BaseXtree(name) => {
364                    ToolOutputPathKind::BaseXleak(name.clone())
365                }
366                kind => kind.clone(),
367            },
368            tool: self.tool,
369            baseline_kind: self.baseline_kind.clone(),
370            name: self.name.clone(),
371            dir: self.dir.clone(),
372            modifiers: self.modifiers.clone(),
373        })
374    }
375
376    /// Return the path to the log file for the given `path`
377    ///
378    /// `path` is supposed to be a path to a valid file in the directory of this [`ToolOutputPath`].
379    pub fn log_path_of(&self, path: &Path) -> Option<PathBuf> {
380        let file_name = path.strip_prefix(&self.dir).ok()?;
381        if let Some(suffix) = self.strip_prefix(&file_name.to_string_lossy()) {
382            let caps = REAL_FILENAME_RE.captures(suffix)?;
383            if let Some(kind) = caps.name("type") {
384                match kind.as_str() {
385                    "out" | "xtree" | "xleak" => {
386                        let mut string = self.prefix();
387                        for s in [
388                            caps.name("pid").map(|c| format!(".{}", c.as_str())),
389                            Some(".log".to_owned()),
390                            caps.name("base").map(|c| format!(".{}", c.as_str())),
391                        ]
392                        .iter()
393                        .filter_map(|s| s.as_ref())
394                        {
395                            string.push_str(s);
396                        }
397
398                        return Some(self.dir.join(string));
399                    }
400                    _ => return Some(path.to_owned()),
401                }
402            }
403        }
404
405        None
406    }
407
408    /// If the [`log::Level`] matches dump the content of all output files into the `writer`
409    pub fn dump_log<W>(&self, log_level: log::Level, writer: &mut W) -> Result<()>
410    where
411        W: Write,
412    {
413        if log_enabled!(log_level) {
414            for path in self.real_paths()? {
415                log::log!(
416                    log_level,
417                    "{} log output '{}':",
418                    self.tool.id(),
419                    path.display()
420                );
421
422                let file = File::open(&path).with_context(|| {
423                    format!(
424                        "Error opening {} output file '{}'",
425                        self.tool.id(),
426                        path.display()
427                    )
428                })?;
429
430                let mut reader = BufReader::new(file);
431                std::io::copy(&mut reader, writer)?;
432            }
433        }
434        Ok(())
435    }
436
437    /// This method can only be used to create the path passed to the tools
438    ///
439    /// The modifiers are extrapolated by the tools and won't match any real path name.
440    pub fn extension(&self) -> String {
441        match (&self.kind, self.modifiers.is_empty()) {
442            (ToolOutputPathKind::Out, true) => "out".to_owned(),
443            (ToolOutputPathKind::Out, false) => format!("out.{}", self.modifiers.join(".")),
444            (ToolOutputPathKind::Log, true) => "log".to_owned(),
445            (ToolOutputPathKind::Log, false) => format!("log.{}", self.modifiers.join(".")),
446            (ToolOutputPathKind::OldOut, true) => "out.old".to_owned(),
447            (ToolOutputPathKind::OldOut, false) => format!("out.old.{}", self.modifiers.join(".")),
448            (ToolOutputPathKind::OldLog, true) => "log.old".to_owned(),
449            (ToolOutputPathKind::OldLog, false) => format!("log.old.{}", self.modifiers.join(".")),
450            (ToolOutputPathKind::BaseLog(name), true) => {
451                format!("log.base@{name}")
452            }
453            (ToolOutputPathKind::BaseLog(name), false) => {
454                format!("log.base@{name}.{}", self.modifiers.join("."))
455            }
456            (ToolOutputPathKind::BaseOut(name), true) => format!("out.base@{name}"),
457            (ToolOutputPathKind::BaseOut(name), false) => {
458                format!("out.base@{name}.{}", self.modifiers.join("."))
459            }
460            (ToolOutputPathKind::Xtree, true) => "xtree".to_owned(),
461            (ToolOutputPathKind::Xtree, false) => format!("xtree.{}", self.modifiers.join(".")),
462            (ToolOutputPathKind::OldXtree, true) => "xtree.old".to_owned(),
463            (ToolOutputPathKind::OldXtree, false) => {
464                format!("xtree.old.{}", self.modifiers.join("."))
465            }
466            (ToolOutputPathKind::BaseXtree(name), true) => format!("xtree.base@{name}"),
467            (ToolOutputPathKind::BaseXtree(name), false) => {
468                format!("xtree.base@{name}.{}", self.modifiers.join("."))
469            }
470            (ToolOutputPathKind::Xleak, true) => "xleak".to_owned(),
471            (ToolOutputPathKind::Xleak, false) => format!("xleak.{}", self.modifiers.join(".")),
472            (ToolOutputPathKind::OldXleak, true) => "xleak.old".to_owned(),
473            (ToolOutputPathKind::OldXleak, false) => {
474                format!("xleak.old.{}", self.modifiers.join("."))
475            }
476            (ToolOutputPathKind::BaseXleak(name), true) => format!("xleak.base@{name}"),
477            (ToolOutputPathKind::BaseXleak(name), false) => {
478                format!("xleak.base@{name}.{}", self.modifiers.join("."))
479            }
480        }
481    }
482
483    /// Create new `ToolOutputPath` with `modifiers`
484    #[must_use]
485    pub fn with_modifiers<I, T>(&self, modifiers: T) -> Self
486    where
487        I: Into<String>,
488        T: IntoIterator<Item = I>,
489    {
490        Self {
491            kind: self.kind.clone(),
492            tool: self.tool,
493            baseline_kind: self.baseline_kind.clone(),
494            dir: self.dir.clone(),
495            name: self.name.clone(),
496            modifiers: modifiers.into_iter().map(Into::into).collect(),
497        }
498    }
499
500    /// Return the unexpanded path usable as input for `--callgrind-out-file`, ...
501    ///
502    /// The path returned by this method does not necessarily have to exist and can include
503    /// modifiers like `%p`. Use [`Self::real_paths`] to get the real and existing (possibly
504    /// multiple) paths to the output files of the respective tool.
505    pub fn to_path(&self) -> PathBuf {
506        self.dir.join(format!(
507            "{}.{}.{}",
508            self.tool.id(),
509            self.name,
510            self.extension()
511        ))
512    }
513
514    /// Walk the benchmark directory (non-recursive)
515    pub fn walk_dir(&self) -> Result<impl Iterator<Item = DirEntry>> {
516        std::fs::read_dir(&self.dir)
517            .with_context(|| {
518                format!(
519                    "Failed opening benchmark directory: '{}'",
520                    self.dir.display()
521                )
522            })
523            .map(|i| i.into_iter().filter_map(Result::ok))
524    }
525
526    /// Strip the `<tool>.<name>` prefix from a `file_name`
527    pub fn strip_prefix<'a>(&self, file_name: &'a str) -> Option<&'a str> {
528        file_name.strip_prefix(format!("{}.{}", self.tool.id(), self.name).as_str())
529    }
530
531    /// Return the file name prefix as in `<tool>.<name>`
532    pub fn prefix(&self) -> String {
533        format!("{}.{}", self.tool.id(), self.name)
534    }
535
536    /// Return the `real` paths of a tool's output files
537    ///
538    /// A tool can have many output files so [`Self::to_path`] is not enough
539    #[allow(clippy::case_sensitive_file_extension_comparisons)]
540    pub fn real_paths(&self) -> Result<Vec<PathBuf>> {
541        let mut paths = vec![];
542        for entry in self.walk_dir()? {
543            let file_name = entry.file_name();
544            let file_name = file_name.to_string_lossy();
545
546            // Silently ignore all paths which don't follow this scheme, for example
547            // (`summary.json`)
548            if let Some(suffix) = self.strip_prefix(&file_name) {
549                let is_match = match &self.kind {
550                    ToolOutputPathKind::Out => suffix.ends_with(".out"),
551                    ToolOutputPathKind::Log => suffix.ends_with(".log"),
552                    ToolOutputPathKind::OldOut => suffix.ends_with(".out.old"),
553                    ToolOutputPathKind::OldLog => suffix.ends_with(".log.old"),
554                    ToolOutputPathKind::BaseLog(name) => {
555                        suffix.ends_with(format!(".log.base@{name}").as_str())
556                    }
557                    ToolOutputPathKind::BaseOut(name) => {
558                        suffix.ends_with(format!(".out.base@{name}").as_str())
559                    }
560                    ToolOutputPathKind::Xtree => suffix.ends_with(".xtree"),
561                    ToolOutputPathKind::OldXtree => suffix.ends_with(".xtree.old"),
562                    ToolOutputPathKind::BaseXtree(name) => {
563                        suffix.ends_with(format!(".xtree.base@{name}").as_str())
564                    }
565                    ToolOutputPathKind::Xleak => suffix.ends_with(".xleak"),
566                    ToolOutputPathKind::OldXleak => suffix.ends_with(".xleak.old"),
567                    ToolOutputPathKind::BaseXleak(name) => {
568                        suffix.ends_with(format!(".xleak.base@{name}").as_str())
569                    }
570                };
571
572                if is_match {
573                    paths.push(entry.path());
574                }
575            }
576        }
577        Ok(paths)
578    }
579
580    /// Return the real paths with their respective modifiers if present
581    pub fn real_paths_with_modifier(&self) -> Result<Vec<(PathBuf, Option<String>)>> {
582        let mut paths = vec![];
583        for entry in self.walk_dir()? {
584            let file_name = entry.file_name().to_string_lossy().to_string();
585
586            // Silently ignore all paths which don't follow this scheme, for example
587            // (`summary.json`)
588            if let Some(suffix) = self.strip_prefix(&file_name) {
589                let modifiers = match &self.kind {
590                    ToolOutputPathKind::Out => suffix.strip_suffix(".out"),
591                    ToolOutputPathKind::Log => suffix.strip_suffix(".log"),
592                    ToolOutputPathKind::OldOut => suffix.strip_suffix(".out.old"),
593                    ToolOutputPathKind::OldLog => suffix.strip_suffix(".log.old"),
594                    ToolOutputPathKind::BaseLog(name) => {
595                        suffix.strip_suffix(format!(".log.base@{name}").as_str())
596                    }
597                    ToolOutputPathKind::BaseOut(name) => {
598                        suffix.strip_suffix(format!(".out.base@{name}").as_str())
599                    }
600                    ToolOutputPathKind::Xtree => suffix.strip_suffix(".xtree"),
601                    ToolOutputPathKind::OldXtree => suffix.strip_suffix(".xtree.old"),
602                    ToolOutputPathKind::BaseXtree(name) => {
603                        suffix.strip_suffix(format!(".xtree.base@{name}").as_str())
604                    }
605                    ToolOutputPathKind::Xleak => suffix.strip_suffix(".xleak"),
606                    ToolOutputPathKind::OldXleak => suffix.strip_suffix(".xleak.old"),
607                    ToolOutputPathKind::BaseXleak(name) => {
608                        suffix.strip_suffix(format!(".xleak.base@{name}").as_str())
609                    }
610                };
611
612                paths.push((
613                    entry.path(),
614                    modifiers.and_then(|s| (!s.is_empty()).then(|| s.to_owned())),
615                ));
616            }
617        }
618        Ok(paths)
619    }
620
621    /// Sanitize callgrind output file names
622    ///
623    /// This method will remove empty files which are occasionally produced by callgrind and only
624    /// cause problems in the parser. The files are renamed from the callgrind file naming scheme to
625    /// ours which is easier to handle.
626    ///
627    /// The information about pids, parts and threads is obtained by parsing the header from the
628    /// callgrind output files instead of relying on the sometimes flaky file names produced by
629    /// `callgrind`. The header is around 10-20 lines, so this method should be still sufficiently
630    /// fast. Additionally, `callgrind` might change the naming scheme of its files, so using the
631    /// headers makes us more independent of a specific valgrind/callgrind version.
632    #[allow(clippy::too_many_lines)]
633    pub fn sanitize_callgrind(&self) -> Result<()> {
634        // path, part
635        type Grouped = (PathBuf, Option<u64>);
636        // base (i.e. base@default) => pid => thread => vec: path, part
637        type Group =
638            HashMap<Option<String>, HashMap<Option<i32>, HashMap<Option<usize>, Vec<Grouped>>>>;
639
640        // To figure out if there are multiple pids/parts/threads present, it's necessary to group
641        // the files in this map. The order doesn't matter since we only rename the original file
642        // names, which doesn't need to follow a specific order.
643        //
644        // At first, we group by (out|log), then base, then pid and then by part in different
645        // hashmaps. The threads are grouped in a vector.
646        let mut groups: HashMap<String, Group> = HashMap::new();
647
648        for entry in self.walk_dir()? {
649            let file_name = entry.file_name();
650            let file_name = file_name.to_string_lossy();
651
652            let Some(haystack) = self.strip_prefix(&file_name) else {
653                continue;
654            };
655
656            if let Some(caps) = CALLGRIND_ORIG_FILENAME_RE.captures(haystack) {
657                // Callgrind sometimes creates empty files for no reason. We clean them
658                // up here
659                if entry.metadata()?.size() == 0 {
660                    std::fs::remove_file(entry.path())?;
661                    continue;
662                }
663
664                // We don't sanitize old files. It's not needed if the new files are always
665                // sanitized. However, we do sanitize `base@<name>` file names.
666                let base = if let Some(base) = caps.name("base") {
667                    if base.as_str() == ".old" {
668                        continue;
669                    }
670
671                    Some(base.as_str().to_owned())
672                } else {
673                    None
674                };
675
676                let out_type = caps
677                    .name("type")
678                    .expect("A out|log type should be present")
679                    .as_str();
680
681                if out_type == ".out" {
682                    let properties = parse_header(
683                        &mut BufReader::new(File::open(entry.path())?)
684                            .lines()
685                            .map(Result::unwrap),
686                    )?;
687                    if let Some(bases) = groups.get_mut(out_type) {
688                        if let Some(pids) = bases.get_mut(&base) {
689                            if let Some(threads) = pids.get_mut(&properties.pid) {
690                                if let Some(parts) = threads.get_mut(&properties.thread) {
691                                    parts.push((entry.path(), properties.part));
692                                } else {
693                                    threads.insert(
694                                        properties.thread,
695                                        vec![(entry.path(), properties.part)],
696                                    );
697                                }
698                            } else {
699                                pids.insert(
700                                    properties.pid,
701                                    HashMap::from([(
702                                        properties.thread,
703                                        vec![(entry.path(), properties.part)],
704                                    )]),
705                                );
706                            }
707                        } else {
708                            bases.insert(
709                                base.clone(),
710                                HashMap::from([(
711                                    properties.pid,
712                                    HashMap::from([(
713                                        properties.thread,
714                                        vec![(entry.path(), properties.part)],
715                                    )]),
716                                )]),
717                            );
718                        }
719                    } else {
720                        groups.insert(
721                            out_type.to_owned(),
722                            HashMap::from([(
723                                base.clone(),
724                                HashMap::from([(
725                                    properties.pid,
726                                    HashMap::from([(
727                                        properties.thread,
728                                        vec![(entry.path(), properties.part)],
729                                    )]),
730                                )]),
731                            )]),
732                        );
733                    }
734                } else {
735                    let pid = caps.name("pid").map(|m| {
736                        m.as_str()[2..]
737                            .parse::<i32>()
738                            .expect("The pid from the match should be number")
739                    });
740
741                    // The log files don't expose any information about parts or threads, so
742                    // these are grouped under the `None` key
743                    if let Some(bases) = groups.get_mut(out_type) {
744                        if let Some(pids) = bases.get_mut(&base) {
745                            if let Some(threads) = pids.get_mut(&pid) {
746                                if let Some(parts) = threads.get_mut(&None) {
747                                    parts.push((entry.path(), None));
748                                } else {
749                                    threads.insert(None, vec![(entry.path(), None)]);
750                                }
751                            } else {
752                                pids.insert(
753                                    pid,
754                                    HashMap::from([(None, vec![(entry.path(), None)])]),
755                                );
756                            }
757                        } else {
758                            bases.insert(
759                                base.clone(),
760                                HashMap::from([(
761                                    pid,
762                                    HashMap::from([(None, vec![(entry.path(), None)])]),
763                                )]),
764                            );
765                        }
766                    } else {
767                        groups.insert(
768                            out_type.to_owned(),
769                            HashMap::from([(
770                                base.clone(),
771                                HashMap::from([(
772                                    pid,
773                                    HashMap::from([(None, vec![(entry.path(), None)])]),
774                                )]),
775                            )]),
776                        );
777                    }
778                }
779            }
780        }
781
782        for (out_type, types) in groups {
783            for (base, bases) in types {
784                let multiple_pids = bases.len() > 1;
785
786                for (pid, threads) in bases {
787                    let multiple_threads = threads.len() > 1;
788
789                    for (thread, parts) in &threads {
790                        let multiple_parts = parts.len() > 1;
791
792                        for (orig_path, part) in parts {
793                            let mut new_file_name = self.prefix();
794
795                            if multiple_pids {
796                                if let Some(pid) = pid {
797                                    write!(new_file_name, ".{pid}").unwrap();
798                                }
799                            }
800
801                            if multiple_threads {
802                                if let Some(thread) = thread {
803                                    let width = threads.len().ilog10() as usize + 1;
804                                    write!(new_file_name, ".t{thread:0width$}").unwrap();
805                                }
806
807                                if !multiple_parts {
808                                    if let Some(part) = part {
809                                        let width = parts.len().ilog10() as usize + 1;
810                                        write!(new_file_name, ".p{part:0width$}").unwrap();
811                                    }
812                                }
813                            }
814
815                            if multiple_parts {
816                                if !multiple_threads {
817                                    if let Some(thread) = thread {
818                                        let width = threads.len().ilog10() as usize + 1;
819                                        write!(new_file_name, ".t{thread:0width$}").unwrap();
820                                    }
821                                }
822
823                                if let Some(part) = part {
824                                    let width = parts.len().ilog10() as usize + 1;
825                                    write!(new_file_name, ".p{part:0width$}").unwrap();
826                                }
827                            }
828
829                            new_file_name.push_str(&out_type);
830                            if let Some(base) = &base {
831                                new_file_name.push_str(base);
832                            }
833
834                            let from = orig_path;
835                            let to = from.with_file_name(new_file_name);
836
837                            std::fs::rename(from, to)?;
838                        }
839                    }
840                }
841            }
842        }
843
844        Ok(())
845    }
846
847    /// Sanitize bbv file names
848    ///
849    /// The original output files of bb have a `.<number>` suffix if there are multiple threads. We
850    /// need the threads as `t<number>` in the modifier part of the final file names.
851    ///
852    /// For example: (orig -> sanitized)
853    ///
854    /// If there are multiple threads, the bb output file name doesn't include the first thread:
855    ///
856    /// `exp-bbv.bench_thread_in_subprocess.548365.bb.out` ->
857    /// `exp-bbv.bench_thread_in_subprocess.548365.t1.bb.out`
858    ///
859    /// `exp-bbv.bench_thread_in_subprocess.548365.bb.out.2` ->
860    /// `exp-bbv.bench_thread_in_subprocess.548365.t2.bb.out`
861    #[allow(clippy::case_sensitive_file_extension_comparisons)]
862    #[allow(clippy::too_many_lines)]
863    pub fn sanitize_bbv(&self) -> Result<()> {
864        // path, thread,
865        type Grouped = (PathBuf, String);
866        // key: bbv_type => key: pid
867        type Group =
868            HashMap<Option<String>, HashMap<Option<String>, HashMap<Option<String>, Vec<Grouped>>>>;
869
870        // key: .(out|log)
871        let mut groups: HashMap<String, Group> = HashMap::new();
872        for entry in self.walk_dir()? {
873            let file_name = entry.file_name();
874            let file_name = file_name.to_string_lossy();
875
876            let Some(haystack) = self.strip_prefix(&file_name) else {
877                continue;
878            };
879
880            if let Some(caps) = BBV_ORIG_FILENAME_RE.captures(haystack) {
881                if entry.metadata()?.size() == 0 {
882                    std::fs::remove_file(entry.path())?;
883                    continue;
884                }
885
886                // Don't sanitize old files.
887                let base = if let Some(base) = caps.name("base") {
888                    if base.as_str() == ".old" {
889                        continue;
890                    }
891
892                    Some(base.as_str().to_owned())
893                } else {
894                    None
895                };
896
897                let out_type = caps.name("type").unwrap().as_str();
898                let bbv_type = caps.name("bbv_type").map(|m| m.as_str().to_owned());
899                let pid = caps.name("pid").map(|p| format!(".{}", &p.as_str()[2..]));
900
901                let thread = caps
902                    .name("thread")
903                    .map_or_else(|| ".1".to_owned(), |t| t.as_str().to_owned());
904
905                if let Some(bases) = groups.get_mut(out_type) {
906                    if let Some(bbv_types) = bases.get_mut(&base) {
907                        if let Some(pids) = bbv_types.get_mut(&bbv_type) {
908                            if let Some(threads) = pids.get_mut(&pid) {
909                                threads.push((entry.path(), thread));
910                            } else {
911                                pids.insert(pid, vec![(entry.path(), thread)]);
912                            }
913                        } else {
914                            bbv_types.insert(
915                                bbv_type.clone(),
916                                HashMap::from([(pid, vec![(entry.path(), thread)])]),
917                            );
918                        }
919                    } else {
920                        bases.insert(
921                            base.clone(),
922                            HashMap::from([(
923                                bbv_type.clone(),
924                                HashMap::from([(pid, vec![(entry.path(), thread)])]),
925                            )]),
926                        );
927                    }
928                } else {
929                    groups.insert(
930                        out_type.to_owned(),
931                        HashMap::from([(
932                            base.clone(),
933                            HashMap::from([(
934                                bbv_type.clone(),
935                                HashMap::from([(pid, vec![(entry.path(), thread)])]),
936                            )]),
937                        )]),
938                    );
939                }
940            }
941        }
942
943        for (out_type, bases) in groups {
944            for (base, bbv_types) in bases {
945                for (bbv_type, pids) in &bbv_types {
946                    let multiple_pids = pids.len() > 1;
947
948                    for (pid, threads) in pids {
949                        let multiple_threads = threads.len() > 1;
950
951                        for (orig_path, thread) in threads {
952                            let mut new_file_name = self.prefix();
953
954                            if multiple_pids {
955                                if let Some(pid) = pid.as_ref() {
956                                    write!(new_file_name, "{pid}").unwrap();
957                                }
958                            }
959
960                            if multiple_threads
961                                && bbv_type.as_ref().is_some_and(|b| b.starts_with(".bb"))
962                            {
963                                let width = threads.len().ilog10() as usize + 1;
964
965                                let thread = thread[1..]
966                                    .parse::<usize>()
967                                    .expect("The thread from the regex should be a number");
968
969                                write!(new_file_name, ".t{thread:0width$}").unwrap();
970                            }
971
972                            if let Some(bbv_type) = &bbv_type {
973                                new_file_name.push_str(bbv_type);
974                            }
975
976                            new_file_name.push_str(&out_type);
977
978                            if let Some(base) = &base {
979                                new_file_name.push_str(base);
980                            }
981
982                            let from = orig_path;
983                            let to = from.with_file_name(new_file_name);
984
985                            std::fs::rename(from, to)?;
986                        }
987                    }
988                }
989            }
990        }
991
992        Ok(())
993    }
994
995    /// Sanitize file names of all tools if not sanitized by a more specific method
996    ///
997    /// The pids are removed from the file name if there was only a single process (pid).
998    /// Additionally, we check for empty files and remove them.
999    pub fn sanitize_generic(&self) -> Result<()> {
1000        // key: base => vec: path, pid
1001        type Group = HashMap<Option<String>, Vec<(PathBuf, Option<String>)>>;
1002
1003        // key: .(out|log|xtree|xleak)
1004        let mut groups: HashMap<String, Group> = HashMap::new();
1005        for entry in self.walk_dir()? {
1006            let file_name = entry.file_name();
1007            let file_name = file_name.to_string_lossy();
1008
1009            let Some(haystack) = self.strip_prefix(&file_name) else {
1010                continue;
1011            };
1012
1013            if let Some(caps) = GENERIC_ORIG_FILENAME_RE.captures(haystack) {
1014                if entry.metadata()?.size() == 0 {
1015                    std::fs::remove_file(entry.path())?;
1016                    continue;
1017                }
1018
1019                // Don't sanitize old files.
1020                let base = if let Some(base) = caps.name("base") {
1021                    if base.as_str() == ".old" {
1022                        continue;
1023                    }
1024
1025                    Some(base.as_str().to_owned())
1026                } else {
1027                    None
1028                };
1029
1030                let out_type = caps.name("type").unwrap().as_str();
1031                let pid = caps.name("pid").map(|p| format!(".{}", &p.as_str()[2..]));
1032
1033                if let Some(bases) = groups.get_mut(out_type) {
1034                    if let Some(pids) = bases.get_mut(&base) {
1035                        pids.push((entry.path(), pid));
1036                    } else {
1037                        bases.insert(base, vec![(entry.path(), pid)]);
1038                    }
1039                } else {
1040                    groups.insert(
1041                        out_type.to_owned(),
1042                        HashMap::from([(base, vec![(entry.path(), pid)])]),
1043                    );
1044                }
1045            }
1046        }
1047
1048        for (out_type, bases) in groups {
1049            for (base, pids) in bases {
1050                let multiple_pids = pids.len() > 1;
1051                for (orig_path, pid) in pids {
1052                    let mut new_file_name = self.prefix();
1053
1054                    if multiple_pids {
1055                        if let Some(pid) = pid.as_ref() {
1056                            write!(new_file_name, "{pid}").unwrap();
1057                        }
1058                    }
1059
1060                    new_file_name.push_str(&out_type);
1061
1062                    if let Some(base) = &base {
1063                        new_file_name.push_str(base);
1064                    }
1065
1066                    let from = orig_path;
1067                    let to = from.with_file_name(new_file_name);
1068
1069                    std::fs::rename(from, to)?;
1070                }
1071            }
1072        }
1073
1074        Ok(())
1075    }
1076
1077    /// Sanitize file names for a specific tool
1078    ///
1079    /// Empty files are cleaned up. For more details on a specific tool see the respective
1080    /// sanitize_<tool> method.
1081    pub fn sanitize(&self) -> Result<()> {
1082        match self.tool {
1083            ValgrindTool::Callgrind => self.sanitize_callgrind()?,
1084            ValgrindTool::BBV => self.sanitize_bbv()?,
1085            _ => self.sanitize_generic()?,
1086        }
1087
1088        Ok(())
1089    }
1090}
1091
1092impl Display for ToolOutputPath {
1093    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1094        f.write_fmt(format_args!("{}", self.to_path().display()))
1095    }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100
1101    use rstest::rstest;
1102
1103    use super::*;
1104
1105    #[rstest]
1106    #[case::out(".out")]
1107    #[case::out_with_pid(".out.#1234")]
1108    #[case::out_with_part(".out.1")]
1109    #[case::out_with_thread(".out-01")]
1110    #[case::out_with_part_and_thread(".out.1-01")]
1111    #[case::out_base(".out.base@default")]
1112    #[case::out_base_with_pid(".out.base@default.#1234")]
1113    #[case::out_base_with_part(".out.base@default.1")]
1114    #[case::out_base_with_thread(".out.base@default-01")]
1115    #[case::out_base_with_part_and_thread(".out.base@default.1-01")]
1116    #[case::log(".log")]
1117    #[case::log_with_pid(".log.#1234")]
1118    #[case::log_base(".log.base@default")]
1119    #[case::log_base_with_pid(".log.base@default.#1234")]
1120    fn test_callgrind_filename_regex(#[case] haystack: &str) {
1121        assert!(CALLGRIND_ORIG_FILENAME_RE.is_match(haystack));
1122    }
1123
1124    #[rstest]
1125    #[case::bb_out(".out.bb")]
1126    #[case::bb_out_with_pid(".out.bb.#1234")]
1127    #[case::bb_out_with_pid_and_thread(".out.bb.#1234.1")]
1128    #[case::bb_out_with_thread(".out.bb.1")]
1129    #[case::pc_out(".out.pc")]
1130    #[case::log(".log")]
1131    #[case::log_with_pid(".log.#1234")]
1132    fn test_bbv_filename_regex(#[case] haystack: &str) {
1133        assert!(BBV_ORIG_FILENAME_RE.is_match(haystack));
1134    }
1135
1136    #[rstest]
1137    #[case::out(".out", vec![("type", "out")])]
1138    #[case::pid_out(".2049595.out", vec![("pid", "2049595"), ("type", "out")])]
1139    #[case::pid_thread_out(".2049595.t1.out", vec![("pid", "2049595"), ("tid", "1"), ("type", "out")])]
1140    #[case::pid_thread_part_out(".2049595.t1.p1.out", vec![("pid", "2049595"), ("tid", "1"), ("part", "1"), ("type", "out")])]
1141    #[case::out_old(".out.old", vec![("type", "out"), ("base", "old")])]
1142    #[case::pid_out_old(".2049595.out.old", vec![("pid", "2049595"), ("type", "out"), ("base", "old")])]
1143    #[case::pid_thread_out_old(".2049595.t1.out.old", vec![("pid", "2049595"), ("tid", "1"), ("type", "out"), ("base", "old")])]
1144    #[case::pid_thread_part_out_old(".2049595.t1.p1.out.old", vec![("pid", "2049595"), ("tid", "1"), ("part", "1"), ("type", "out"), ("base", "old")])]
1145    #[case::out_base(".out.base@name", vec![("type", "out"), ("base", "base@name")])]
1146    #[case::pid_out_base(".2049595.out.base@name", vec![("pid", "2049595"), ("type", "out"), ("base", "base@name")])]
1147    #[case::pid_thread_out_base(".2049595.t1.out.base@name", vec![("pid", "2049595"), ("tid", "1"), ("type", "out"), ("base", "base@name")])]
1148    #[case::pid_thread_part_out_base(".2049595.t1.p1.out.base@name", vec![("pid", "2049595"), ("tid", "1"), ("part", "1"), ("type", "out"), ("base", "base@name")])]
1149    #[case::bb_out(".bb.out", vec![("bbv", "bb"), ("type", "out")])]
1150    #[case::pc_out(".pc.out", vec![("bbv", "pc"), ("type", "out")])]
1151    #[case::pid_bb_out(".123.bb.out", vec![("pid", "123"), ("bbv", "bb"), ("type", "out")])]
1152    #[case::pid_thread_bb_out(".123.t1.bb.out", vec![("pid", "123"), ("tid", "1"), ("bbv", "bb"), ("type", "out")])]
1153    #[case::log(".log", vec![("type", "log")])]
1154    #[case::xtree(".xtree", vec![("type", "xtree")])]
1155    #[case::xtree_old(".xtree.old", vec![("type", "xtree"), ("base", "old")])]
1156    #[case::xleak(".xleak", vec![("type", "xleak")])]
1157    #[case::xleak_old(".xleak.old", vec![("type", "xleak"), ("base", "old")])]
1158    fn test_real_file_name_regex(#[case] haystack: &str, #[case] expected: Vec<(&str, &str)>) {
1159        assert!(REAL_FILENAME_RE.is_match(haystack));
1160
1161        let caps = REAL_FILENAME_RE.captures(haystack).unwrap();
1162        for (name, value) in expected {
1163            assert_eq!(caps.name(name).unwrap().as_str(), value);
1164        }
1165    }
1166
1167    #[rstest]
1168    #[case::out(
1169        ValgrindTool::Callgrind,
1170        "callgrind.bench_thread_in_subprocess.two.out",
1171        "callgrind.bench_thread_in_subprocess.two.log"
1172    )]
1173    #[case::out_old(
1174        ValgrindTool::Callgrind,
1175        "callgrind.bench_thread_in_subprocess.two.out.old",
1176        "callgrind.bench_thread_in_subprocess.two.log.old"
1177    )]
1178    #[case::pid_out(
1179        ValgrindTool::Callgrind,
1180        "callgrind.bench_thread_in_subprocess.two.123.out",
1181        "callgrind.bench_thread_in_subprocess.two.123.log"
1182    )]
1183    #[case::pid_tid_out(
1184        ValgrindTool::Callgrind,
1185        "callgrind.bench_thread_in_subprocess.two.123.t1.out",
1186        "callgrind.bench_thread_in_subprocess.two.123.log"
1187    )]
1188    #[case::pid_tid_part_out(
1189        ValgrindTool::Callgrind,
1190        "callgrind.bench_thread_in_subprocess.two.123.t1.p2.out",
1191        "callgrind.bench_thread_in_subprocess.two.123.log"
1192    )]
1193    #[case::pid_out_old(
1194        ValgrindTool::Callgrind,
1195        "callgrind.bench_thread_in_subprocess.two.123.out.old",
1196        "callgrind.bench_thread_in_subprocess.two.123.log.old"
1197    )]
1198    #[case::pid_tid_part_out_old(
1199        ValgrindTool::Callgrind,
1200        "callgrind.bench_thread_in_subprocess.two.123.t1.p2.out.old",
1201        "callgrind.bench_thread_in_subprocess.two.123.log.old"
1202    )]
1203    #[case::bb_out(
1204        ValgrindTool::BBV,
1205        "exp-bbv.bench_thread_in_subprocess.two.bb.out",
1206        "exp-bbv.bench_thread_in_subprocess.two.log"
1207    )]
1208    #[case::bb_pid_out(
1209        ValgrindTool::BBV,
1210        "exp-bbv.bench_thread_in_subprocess.two.123.bb.out",
1211        "exp-bbv.bench_thread_in_subprocess.two.123.log"
1212    )]
1213    #[case::bb_pid_tid_out(
1214        ValgrindTool::BBV,
1215        "exp-bbv.bench_thread_in_subprocess.two.123.t1.bb.out",
1216        "exp-bbv.bench_thread_in_subprocess.two.123.log"
1217    )]
1218    #[case::xtree(
1219        ValgrindTool::Memcheck,
1220        "memcheck.bench_thread_in_subprocess.two.xtree",
1221        "memcheck.bench_thread_in_subprocess.two.log"
1222    )]
1223    #[case::xtree_old(
1224        ValgrindTool::Memcheck,
1225        "memcheck.bench_thread_in_subprocess.two.xtree.old",
1226        "memcheck.bench_thread_in_subprocess.two.log.old"
1227    )]
1228    #[case::xtree_pid(
1229        ValgrindTool::Memcheck,
1230        "memcheck.bench_thread_in_subprocess.two.123.xtree",
1231        "memcheck.bench_thread_in_subprocess.two.123.log"
1232    )]
1233    #[case::xleak(
1234        ValgrindTool::Memcheck,
1235        "memcheck.bench_thread_in_subprocess.two.xleak",
1236        "memcheck.bench_thread_in_subprocess.two.log"
1237    )]
1238    #[case::xleak_old(
1239        ValgrindTool::Memcheck,
1240        "memcheck.bench_thread_in_subprocess.two.xleak.old",
1241        "memcheck.bench_thread_in_subprocess.two.log.old"
1242    )]
1243    #[case::xleak_pid(
1244        ValgrindTool::Memcheck,
1245        "memcheck.bench_thread_in_subprocess.two.123.xleak",
1246        "memcheck.bench_thread_in_subprocess.two.123.log"
1247    )]
1248    fn test_tool_output_path_log_path_of(
1249        #[case] tool: ValgrindTool,
1250        #[case] input: PathBuf,
1251        #[case] expected: PathBuf,
1252    ) {
1253        let output_path = ToolOutputPath::new(
1254            ToolOutputPathKind::Out,
1255            tool,
1256            &BaselineKind::Old,
1257            &PathBuf::from("/root"),
1258            &ModulePath::new("hello::world"),
1259            "bench_thread_in_subprocess.two",
1260        );
1261        let expected = output_path.dir.join(expected);
1262        let actual = output_path
1263            .log_path_of(&output_path.dir.join(input))
1264            .unwrap();
1265
1266        assert_eq!(actual, expected);
1267    }
1268
1269    #[test]
1270    fn test_tool_output_path_log_path_of_when_log_then_same() {
1271        let output_path = ToolOutputPath::new(
1272            ToolOutputPathKind::Log,
1273            ValgrindTool::Callgrind,
1274            &BaselineKind::Old,
1275            &PathBuf::from("/root"),
1276            &ModulePath::new("hello::world"),
1277            "bench_thread_in_subprocess.two",
1278        );
1279        let path = PathBuf::from(
1280            "/root/hello/world/bench_thread_in_subprocess.two/callgrind.\
1281             bench_thread_in_subprocess.two.log",
1282        );
1283
1284        assert_eq!(output_path.log_path_of(&path), Some(path));
1285    }
1286
1287    #[test]
1288    fn test_tool_output_path_log_path_of_when_not_in_dir_then_none() {
1289        let output_path = ToolOutputPath::new(
1290            ToolOutputPathKind::Out,
1291            ValgrindTool::Callgrind,
1292            &BaselineKind::Old,
1293            &PathBuf::from("/root"),
1294            &ModulePath::new("hello::world"),
1295            "bench_thread_in_subprocess.two",
1296        );
1297
1298        assert!(output_path
1299            .log_path_of(&PathBuf::from(
1300                "/root/not/here/bench_thread_in_subprocess.two/callgrind.\
1301                 bench_thread_in_subprocess.two.out"
1302            ))
1303            .is_none());
1304    }
1305}