1pub 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 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 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 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 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#[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 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 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 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 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 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 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 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 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 pub fn prefix(&self) -> String {
801 format!("{}.{}", self.tool.id(), self.name)
802 }
803
804 #[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 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 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 pub fn sanitize_callgrind(&self) -> Result<()> {
880 type Grouped = (PathBuf, Option<u64>);
882 type Group =
884 HashMap<Option<String>, HashMap<Option<i32>, HashMap<Option<usize>, Vec<Grouped>>>>;
885
886 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 if entry.metadata()?.size() == 0 {
906 std::fs::remove_file(entry.path())?;
907 continue;
908 }
909
910 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 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 #[allow(clippy::case_sensitive_file_extension_comparisons)]
1108 pub fn sanitize_bbv(&self) -> Result<()> {
1109 type Grouped = (PathBuf, String);
1111 type Group =
1113 HashMap<Option<String>, HashMap<Option<String>, HashMap<Option<String>, Vec<Grouped>>>>;
1114
1115 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 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 pub fn sanitize_generic(&self) -> Result<()> {
1245 type Group = HashMap<Option<String>, Vec<(PathBuf, Option<String>)>>;
1247
1248 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 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 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 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}