iai_callgrind_runner/runner/
format.rs

1//! The format of Iai-Callgrind terminal output
2//!
3//! All direct print statements should be part of this module and there should be no `println!` or
4//! similar statement in any other module of the runner.
5use std::borrow::Cow;
6use std::fmt::{Display, Write};
7use std::path::PathBuf;
8
9use anyhow::Result;
10use colored::{Color, ColoredString, Colorize};
11use indexmap::{indexset, IndexSet};
12
13use super::args::NoCapture;
14use super::bin_bench::BinBench;
15use super::common::{Baselines, BenchmarkSummaries, Config, ModulePath};
16use super::lib_bench::LibBench;
17use super::meta::Metadata;
18use super::metrics::{Metric, MetricKind, MetricsDiff};
19use super::summary::{Diffs, ProfileData, ProfileInfo, ToolMetricSummary, ToolRegression};
20use crate::api::{
21    self, CachegrindMetric, CachegrindMetrics, CallgrindMetrics, DhatMetric, DhatMetrics,
22    ErrorMetric, EventKind, Tool, ToolOutputFormat, ValgrindTool,
23};
24use crate::util::{
25    make_relative, to_string_signed_short, to_string_unsigned_short, truncate_str_utf8,
26    EitherOrBoth,
27};
28
29/// The width in bytes of the difference (and factor)
30pub const DIFF_WIDTH: usize = 9;
31/// The width in bytes of the FIELD as in `  FIELD: METRIC | METRIC (DIFF_PCT) [FACTOR]`
32pub const FIELD_WIDTH: usize = 21;
33/// The `DIFF_WIDTH` - the length of the unit
34pub const FLOAT_WIDTH: usize = DIFF_WIDTH - 1;
35/// The width in bytes of the "left" side of the separator `|`
36pub const LEFT_WIDTH: usize = METRIC_WIDTH + FIELD_WIDTH;
37#[allow(clippy::doc_link_with_quotes)]
38/// The maximum line width
39///
40/// indent + left + "|" + metric width + " " + "(" + percentage + ")" + " " + "[" + factor + "]"
41pub const MAX_WIDTH: usize = 2 + LEFT_WIDTH + 1 + METRIC_WIDTH + 2 * 11;
42/// The width in bytes of the metric
43pub const METRIC_WIDTH: usize = 20;
44/// The string used to signal that a value is not available
45pub const NOT_AVAILABLE: &str = "N/A";
46/// Used to indicate that there is no difference between the `new` and `old` metric
47pub const NO_CHANGE: &str = "No change";
48/// The string used in the difference when there is no difference to show
49pub const UNKNOWN: &str = "*********";
50/// The string used to signal that the difference is in the tolerance margin
51pub const WITHIN_TOLERANCE: &str = "Tolerance";
52
53enum IndentKind {
54    Normal,
55    ToolHeadline,
56    ToolSubHeadline,
57}
58
59/// The kind of the output format can be either json or the default terminal output
60#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
61pub enum OutputFormatKind {
62    /// The default terminal output
63    #[default]
64    Default,
65    /// Json terminal output
66    Json,
67    /// Pretty json terminal output
68    PrettyJson,
69}
70
71/// The first line and header of a binary benchmark run
72///
73/// For example `module::path id: some args`
74pub struct BinaryBenchmarkHeader {
75    inner: Header,
76    output_format: OutputFormat,
77}
78
79/// The header of the comparison between two different benchmarks
80pub struct ComparisonHeader {
81    /// The details to print in addition or instead of the metrics
82    pub details: Option<String>,
83    /// The function name of the other benchmark
84    pub function_name: String,
85    /// The id of the other benchmark.
86    pub id: String,
87    /// The indentation depending on the output format with grid or without
88    pub indent: String,
89}
90
91/// The first line and header of a benchmark run
92pub struct Header {
93    description: Option<String>,
94    id: Option<String>,
95    module_path: String,
96}
97/// The first line and header of a library benchmark run
98///
99/// For example `module::path id: some args`
100pub struct LibraryBenchmarkHeader {
101    inner: Header,
102    output_format: OutputFormat,
103}
104
105/// The `OutputFormat` of the Iai-Callgrind terminal output
106#[derive(Debug, Clone, PartialEq)]
107pub struct OutputFormat {
108    /// The Cachegrind metrics to show
109    pub cachegrind: IndexSet<CachegrindMetric>,
110    /// The Callgrind metrics to show
111    pub callgrind: IndexSet<EventKind>,
112    /// The DHAT metrics to show
113    pub dhat: IndexSet<DhatMetric>,
114    /// The DRD error metrics to show
115    pub drd: IndexSet<ErrorMetric>,
116    /// The Helgrind error metrics to show
117    pub helgrind: IndexSet<ErrorMetric>,
118    /// The [`OutputFormatKind`]
119    pub kind: OutputFormatKind,
120    /// The Memcheck error metrics to show
121    pub memcheck: IndexSet<ErrorMetric>,
122    /// Show a grid instead of blank spaces
123    pub show_grid: bool,
124    /// Show intermediate metrics output or just the total
125    pub show_intermediate: bool,
126    /// Don't show differences within the tolerance margin
127    pub tolerance: Option<f64>,
128    /// If present truncate the description to this amount of bytes
129    pub truncate_description: Option<usize>,
130}
131
132/// The formatter of the benchmark summary printed after all benchmarks
133#[derive(Debug, Clone)]
134pub struct SummaryFormatter {
135    /// The [`OutputFormatKind`]
136    pub output_format_kind: OutputFormatKind,
137}
138
139/// The main implementation of the [`Formatter`] trait
140#[derive(Debug, Clone)]
141pub struct VerticalFormatter {
142    buffer: String,
143    indent: String,
144    indent_sub_header: String,
145    indent_tool_header: String,
146    output_format: OutputFormat,
147}
148
149/// The trait for the formatter of Iai-Callgrind terminal output and metrics
150pub trait Formatter {
151    /// Clear the buffer
152    fn clear(&mut self);
153
154    /// Format the output the whole [`ProfileData`]
155    fn format(
156        &mut self,
157        tool: ValgrindTool,
158        config: &Config,
159        baselines: &Baselines,
160        data: &ProfileData,
161        is_default_tool: bool,
162    ) -> Result<()>;
163
164    // TODO: Refactor rename to format_line
165    /// Format a line in free form as is
166    fn format_free_form(&mut self, line: &str) -> Result<()>;
167
168    /// Format the output of a single [`ToolMetricSummary`] of a tool
169    fn format_single(
170        &mut self,
171        tool: ValgrindTool,
172        baselines: &Baselines,
173        info: Option<&EitherOrBoth<ProfileInfo>>,
174        metrics_summary: &ToolMetricSummary,
175        is_default_tool: bool,
176    ) -> Result<()>;
177
178    /// Return the [`OutputFormat`] of this formatter
179    fn get_output_format(&self) -> &OutputFormat;
180
181    /// Print the formatted output of the whole [`ProfileData`] if the output format is not json
182    fn print(
183        &mut self,
184        tool: ValgrindTool,
185        config: &Config,
186        baselines: &Baselines,
187        data: &ProfileData,
188        is_default_tool: bool,
189    ) -> Result<()>
190    where
191        Self: std::fmt::Display,
192    {
193        if self.get_output_format().is_default() {
194            self.format(tool, config, baselines, data, is_default_tool)?;
195            print!("{self}");
196            self.clear();
197        }
198        Ok(())
199    }
200
201    /// Print a comparison between two different benchmarks
202    fn print_comparison(
203        &mut self,
204        function_name: &str,
205        id: &str,
206        details: Option<&str>,
207        summaries: Vec<(ValgrindTool, ToolMetricSummary)>,
208    ) -> Result<()>;
209}
210
211impl BinaryBenchmarkHeader {
212    /// Create a new `BinaryBenchmarkHeader`
213    pub fn new(meta: &Metadata, bin_bench: &BinBench) -> Self {
214        let path = make_relative(&meta.project_root, &bin_bench.command.path);
215
216        let command_args: Vec<String> = bin_bench
217            .command
218            .args
219            .iter()
220            .map(|s| s.to_string_lossy().to_string())
221            .collect();
222        let command_args = shlex::try_join(command_args.iter().map(String::as_str)).unwrap();
223
224        let description = if command_args.is_empty() {
225            format!(
226                "({}) -> {}",
227                bin_bench.args.as_ref().map_or("", String::as_str),
228                path.display(),
229            )
230        } else {
231            format!(
232                "({}) -> {} {}",
233                bin_bench.args.as_ref().map_or("", String::as_str),
234                path.display(),
235                command_args
236            )
237        };
238
239        Self {
240            inner: Header::new(
241                &bin_bench.module_path,
242                bin_bench.id.clone(),
243                Some(description),
244                &bin_bench.output_format,
245            ),
246            output_format: bin_bench.output_format.clone(),
247        }
248    }
249
250    /// Print the header
251    pub fn print(&self) {
252        if self.output_format.kind == OutputFormatKind::Default {
253            self.inner.print();
254        }
255    }
256
257    /// Convert the header to a flamegraph title
258    pub fn to_title(&self) -> String {
259        self.inner.to_title()
260    }
261
262    /// Return the description part of the header
263    pub fn description(&self) -> Option<String> {
264        self.inner.description.clone()
265    }
266}
267
268impl ComparisonHeader {
269    /// Create a new `ComparisonHeader`
270    pub fn new<T, U, V>(
271        function_name: T,
272        id: U,
273        details: Option<V>,
274        output_format: &OutputFormat,
275    ) -> Self
276    where
277        T: Into<String>,
278        U: Into<String>,
279        V: Into<String>,
280    {
281        Self {
282            function_name: function_name.into(),
283            id: id.into(),
284            details: details.map(Into::into),
285            indent: if output_format.show_grid {
286                "|-".bright_black().to_string()
287            } else {
288                "  ".to_owned()
289            },
290        }
291    }
292
293    /// Print the header
294    pub fn print(&self) {
295        println!("{self}");
296    }
297}
298
299impl Display for ComparisonHeader {
300    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301        write!(
302            f,
303            "{}{} {} {}",
304            self.indent,
305            "Comparison with".yellow().bold(),
306            self.function_name.green(),
307            self.id.cyan()
308        )?;
309
310        if let Some(details) = &self.details {
311            write!(f, ":{}", details.blue().bold())?;
312        }
313
314        Ok(())
315    }
316}
317
318impl Header {
319    /// Create a new `Header`
320    pub fn new<T>(
321        module_path: &ModulePath,
322        id: T,
323        description: Option<String>,
324        output_format: &OutputFormat,
325    ) -> Self
326    where
327        T: Into<Option<String>>,
328    {
329        let truncated = description
330            .map(|d| truncate_description(&d, output_format.truncate_description).to_string());
331
332        Self {
333            module_path: module_path.to_string(),
334            id: id.into(),
335            description: truncated,
336        }
337    }
338
339    /// Create a new `Header` with a description
340    pub fn without_description<T>(module_path: &ModulePath, id: T) -> Self
341    where
342        T: Into<Option<String>>,
343    {
344        Self {
345            module_path: module_path.to_string(),
346            id: id.into(),
347            description: None,
348        }
349    }
350
351    /// Print the header
352    pub fn print(&self) {
353        println!("{self}");
354    }
355
356    /// Convert the header into a flamegraph title
357    pub fn to_title(&self) -> String {
358        let mut output = String::new();
359
360        write!(output, "{}", self.module_path).unwrap();
361        if let Some(id) = &self.id {
362            match &self.description {
363                Some(description) if !description.is_empty() => {
364                    write!(output, " {id}:{description}").unwrap();
365                }
366                _ => {
367                    write!(output, " {id}").unwrap();
368                }
369            }
370        }
371        output
372    }
373}
374
375impl Display for Header {
376    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377        f.write_fmt(format_args!("{}", self.module_path.green()))?;
378
379        if let Some(id) = &self.id {
380            match &self.description {
381                Some(description) if !description.is_empty() => {
382                    f.write_fmt(format_args!(
383                        " {}{}{}",
384                        id.cyan(),
385                        ":".cyan(),
386                        description.bold().blue(),
387                    ))?;
388                }
389                _ if !id.is_empty() => {
390                    f.write_fmt(format_args!(" {}", id.cyan()))?;
391                }
392                _ => {}
393            }
394        } else if let Some(description) = &self.description {
395            if !description.is_empty() {
396                f.write_fmt(format_args!(" {}", description.bold().blue()))?;
397            }
398        } else {
399            // do nothing
400        }
401        Ok(())
402    }
403}
404
405impl LibraryBenchmarkHeader {
406    /// Create a new `LibraryBenchmarkHeader`
407    pub fn new(lib_bench: &LibBench) -> Self {
408        let header = Header::new(
409            &lib_bench.module_path,
410            lib_bench.id.clone(),
411            lib_bench.args.clone(),
412            &lib_bench.output_format,
413        );
414
415        Self {
416            inner: header,
417            output_format: lib_bench.output_format.clone(),
418        }
419    }
420
421    /// Print the header
422    pub fn print(&self) {
423        if self.output_format.is_default() {
424            self.inner.print();
425        }
426    }
427
428    /// Convert the header into a flamegraph title
429    pub fn to_title(&self) -> String {
430        self.inner.to_title()
431    }
432
433    /// Return the description part of the header if present
434    pub fn description(&self) -> Option<String> {
435        self.inner.description.clone()
436    }
437}
438
439impl OutputFormat {
440    /// Return true if the `OutputFormat` is the default format
441    pub fn is_default(&self) -> bool {
442        self.kind == OutputFormatKind::Default
443    }
444
445    /// Return true if the `OutputFormat` is json
446    pub fn is_json(&self) -> bool {
447        self.kind == OutputFormatKind::Json || self.kind == OutputFormatKind::PrettyJson
448    }
449
450    /// Update the output format from the [`Tool`] if present
451    pub fn update(&mut self, tool: Option<&Tool>) {
452        if let Some(tool) = tool {
453            if let Some(format) = &tool.output_format {
454                match format {
455                    ToolOutputFormat::Callgrind(metrics) => {
456                        self.callgrind = metrics.iter().fold(IndexSet::new(), |mut acc, m| {
457                            acc.extend(IndexSet::from(*m));
458                            acc
459                        });
460                    }
461                    ToolOutputFormat::Cachegrind(metrics) => {
462                        self.cachegrind = metrics.iter().fold(IndexSet::new(), |mut acc, m| {
463                            acc.extend(IndexSet::from(*m));
464                            acc
465                        });
466                    }
467                    ToolOutputFormat::DHAT(metrics) => {
468                        self.dhat = metrics.iter().copied().collect();
469                    }
470                    ToolOutputFormat::Memcheck(metrics) => {
471                        self.memcheck = metrics.iter().copied().collect();
472                    }
473                    ToolOutputFormat::Helgrind(metrics) => {
474                        self.helgrind = metrics.iter().copied().collect();
475                    }
476                    ToolOutputFormat::DRD(metrics) => {
477                        self.drd = metrics.iter().copied().collect();
478                    }
479                    ToolOutputFormat::None => {}
480                }
481            }
482        }
483    }
484
485    /// Update the output format with data from command-line arguments in [`Metadata`]
486    pub fn update_from_meta(&mut self, meta: &Metadata) {
487        if let Some(metrics) = &meta.args.cachegrind_metrics {
488            self.cachegrind.clone_from(metrics);
489        }
490        if let Some(metrics) = &meta.args.callgrind_metrics {
491            self.callgrind.clone_from(metrics);
492        }
493        if let Some(metrics) = &meta.args.dhat_metrics {
494            self.dhat.clone_from(metrics);
495        }
496        if let Some(metrics) = &meta.args.drd_metrics {
497            self.drd.clone_from(metrics);
498        }
499        if let Some(metrics) = &meta.args.helgrind_metrics {
500            self.helgrind.clone_from(metrics);
501        }
502        if let Some(metrics) = &meta.args.memcheck_metrics {
503            self.memcheck.clone_from(metrics);
504        }
505
506        if meta.args.tolerance.is_some() {
507            self.tolerance = meta.args.tolerance;
508        }
509    }
510}
511
512impl Default for OutputFormat {
513    fn default() -> Self {
514        Self {
515            kind: OutputFormatKind::default(),
516            truncate_description: Some(50),
517            show_intermediate: false,
518            show_grid: false,
519            tolerance: None,
520            callgrind: IndexSet::from(CallgrindMetrics::Default),
521            cachegrind: IndexSet::from(CachegrindMetrics::Default),
522            dhat: IndexSet::from(DhatMetrics::Default),
523            memcheck: indexset![
524                ErrorMetric::Errors,
525                ErrorMetric::Contexts,
526                ErrorMetric::SuppressedErrors,
527                ErrorMetric::SuppressedContexts,
528            ],
529            helgrind: indexset![
530                ErrorMetric::Errors,
531                ErrorMetric::Contexts,
532                ErrorMetric::SuppressedErrors,
533                ErrorMetric::SuppressedContexts,
534            ],
535            drd: indexset![
536                ErrorMetric::Errors,
537                ErrorMetric::Contexts,
538                ErrorMetric::SuppressedErrors,
539                ErrorMetric::SuppressedContexts,
540            ],
541        }
542    }
543}
544
545impl From<api::OutputFormat> for OutputFormat {
546    fn from(value: api::OutputFormat) -> Self {
547        Self {
548            kind: OutputFormatKind::Default,
549            truncate_description: value.truncate_description.unwrap_or(Some(50)),
550            show_intermediate: value.show_intermediate.unwrap_or(false),
551            show_grid: value.show_grid.unwrap_or(false),
552            tolerance: value.tolerance,
553            ..Default::default()
554        }
555    }
556}
557
558impl SummaryFormatter {
559    /// Create a new `SummaryFormatter`
560    pub fn new(output_format_kind: OutputFormatKind) -> Self {
561        Self { output_format_kind }
562    }
563
564    /// Print the summary
565    pub fn print(&self, summaries: &BenchmarkSummaries) {
566        if self.output_format_kind == OutputFormatKind::Default {
567            let total_benchmarks = summaries.num_benchmarks();
568            let total_time = to_string_unsigned_short(
569                summaries
570                    .total_time
571                    .expect("The total execution time should be present")
572                    .as_secs_f64(),
573            );
574
575            if summaries.is_regressed() {
576                println!("\nRegressions:\n");
577                let mut num_regressed = 0;
578                for summary in summaries.summaries.iter().filter(|p| p.is_regressed()) {
579                    if let Some(id) = &summary.id {
580                        println!("  {} {}:", summary.module_path.green(), id.cyan());
581                    } else {
582                        println!("  {}:", summary.module_path.green());
583                    }
584                    for regression in summary
585                        .profiles
586                        .iter()
587                        .flat_map(|t| &t.summaries.total.regressions)
588                    {
589                        match regression {
590                            ToolRegression::Soft {
591                                metric,
592                                new,
593                                old,
594                                diff_pct,
595                                limit,
596                            } => {
597                                println!(
598                                    "    {metric} ({} -> {}): {:>6}{} exceeds limit of {:>6}{}",
599                                    old,
600                                    new.to_string().bold(),
601                                    to_string_signed_short(*diff_pct).bright_red().bold(),
602                                    "%".bright_red().bold(),
603                                    to_string_signed_short(*limit).bright_black(),
604                                    "%".bright_black()
605                                );
606                            }
607                            ToolRegression::Hard {
608                                metric,
609                                new,
610                                diff,
611                                limit,
612                            } => {
613                                println!(
614                                    "    {metric} ({0}): {0} exceeds limit of {1} by {2}",
615                                    new.to_string().bold(),
616                                    limit.to_string().bright_black(),
617                                    diff.to_string().bright_red().bold()
618                                );
619                            }
620                        }
621                    }
622
623                    num_regressed += 1;
624                }
625
626                let num_not_regressed = total_benchmarks - num_regressed;
627                println!(
628                    "\nIai-Callgrind result: {}. {num_not_regressed} without regressions; \
629                     {num_regressed} regressed; {total_benchmarks} benchmarks finished in \
630                     {total_time:>6}s",
631                    "Regressed".bright_red().bold(),
632                );
633            } else {
634                println!(
635                    "\nIai-Callgrind result: {}. {total_benchmarks} without regressions; 0 \
636                     regressed; {total_benchmarks} benchmarks finished in {total_time:>6}s",
637                    "Ok".green().bold(),
638                );
639            }
640        }
641    }
642}
643
644impl VerticalFormatter {
645    /// Create a new `VerticalFormatter` (the default format)
646    pub fn new(output_format: OutputFormat) -> Self {
647        if output_format.show_grid {
648            Self {
649                buffer: String::new(),
650                indent: "| ".bright_black().to_string(),
651                indent_sub_header: "|-".bright_black().to_string(),
652                indent_tool_header: "|=".bright_black().to_string(),
653                output_format,
654            }
655        } else {
656            Self {
657                buffer: String::new(),
658                indent: "  ".bright_black().to_string(),
659                indent_sub_header: "  ".bright_black().to_string(),
660                indent_tool_header: "  ".bright_black().to_string(),
661                output_format,
662            }
663        }
664    }
665
666    /// Print the internal buffer as is and clear it afterwards
667    pub fn print_buffer(&mut self) {
668        print!("{}", self.buffer);
669        self.clear();
670    }
671
672    /// Write the indentation depending on the chosen [`OutputFormat`] and [`IndentKind`]
673    fn write_indent(&mut self, kind: &IndentKind) {
674        match kind {
675            IndentKind::Normal => write!(self, "{}", self.indent.clone()).unwrap(),
676            IndentKind::ToolHeadline => {
677                write!(self, "{}", self.indent_tool_header.clone()).unwrap();
678            }
679            IndentKind::ToolSubHeadline => {
680                write!(self, "{}", self.indent_sub_header.clone()).unwrap();
681            }
682        }
683    }
684
685    fn write_field<T>(
686        &mut self,
687        field: &str,
688        values: &EitherOrBoth<T>,
689        color: Option<Color>,
690        left_align: bool,
691    ) where
692        T: AsRef<str>,
693    {
694        self.write_indent(&IndentKind::Normal);
695
696        match values {
697            EitherOrBoth::Left(left) => {
698                let left = left.as_ref();
699                let colored = match color {
700                    Some(color) => left.color(color).bold(),
701                    None => left.bold(),
702                };
703
704                if left_align {
705                    writeln!(self, "{field:<FIELD_WIDTH$}{colored}").unwrap();
706                } else {
707                    writeln!(
708                        self,
709                        "{field:<FIELD_WIDTH$}{}{colored}",
710                        " ".repeat(METRIC_WIDTH.saturating_sub(left.len()))
711                    )
712                    .unwrap();
713                }
714            }
715            EitherOrBoth::Right(right) => {
716                let right = right.as_ref().trim();
717                let colored = match color {
718                    Some(color) => right.color(color),
719                    None => ColoredString::from(right),
720                };
721
722                writeln!(
723                    self,
724                    "{field:<FIELD_WIDTH$}{}|{colored}",
725                    " ".repeat(METRIC_WIDTH),
726                )
727                .unwrap();
728            }
729            EitherOrBoth::Both(left, right) => {
730                let left = left.as_ref().trim();
731                let right = right.as_ref().trim();
732
733                let colored_left = match color {
734                    Some(color) => left.color(color).bold(),
735                    None => left.bold(),
736                };
737                let colored_right = match color {
738                    Some(color) => right.color(color),
739                    None => ColoredString::from(right),
740                };
741
742                if left.len() > METRIC_WIDTH {
743                    writeln!(self, "{field:<FIELD_WIDTH$}{colored_left}").unwrap();
744                    self.write_indent(&IndentKind::Normal);
745                    writeln!(self, "{}|{colored_right}", " ".repeat(LEFT_WIDTH)).unwrap();
746                } else if left_align {
747                    writeln!(
748                        self,
749                        "{field:<FIELD_WIDTH$}{colored_left}{}|{colored_right}",
750                        " ".repeat(METRIC_WIDTH - left.len()),
751                    )
752                    .unwrap();
753                } else {
754                    writeln!(
755                        self,
756                        "{field:<FIELD_WIDTH$}{}{colored_left}|{colored_right}",
757                        " ".repeat(METRIC_WIDTH - left.len()),
758                    )
759                    .unwrap();
760                }
761            }
762        }
763    }
764
765    fn write_metric(&mut self, field: &str, metrics: &EitherOrBoth<&Metric>, diffs: Option<Diffs>) {
766        match metrics {
767            EitherOrBoth::Left(new) => {
768                let right = format!(
769                    "{NOT_AVAILABLE:<METRIC_WIDTH$} ({:^DIFF_WIDTH$})",
770                    UNKNOWN.bright_black()
771                );
772                self.write_field(
773                    field,
774                    &EitherOrBoth::Both(&new.to_string(), &right),
775                    None,
776                    false,
777                );
778            }
779            EitherOrBoth::Right(old) => {
780                let right = format!(
781                    "{old:<METRIC_WIDTH$} ({:^DIFF_WIDTH$})",
782                    UNKNOWN.bright_black()
783                );
784                self.write_field(
785                    field,
786                    &EitherOrBoth::Both(NOT_AVAILABLE, &right),
787                    None,
788                    false,
789                );
790            }
791            EitherOrBoth::Both(new, old) if new == old => {
792                let right = format!(
793                    "{old:<METRIC_WIDTH$} ({:^DIFF_WIDTH$})",
794                    NO_CHANGE.bright_black()
795                );
796                self.write_field(
797                    field,
798                    &EitherOrBoth::Both(&new.to_string(), &right),
799                    None,
800                    false,
801                );
802            }
803            EitherOrBoth::Both(new, old)
804                if self.output_format.tolerance.is_some_and(|tolerance| {
805                    diffs
806                        .map(|diffs| diffs.diff_pct)
807                        .expect("A difference should be present")
808                        .abs()
809                        <= tolerance.abs()
810                }) =>
811            {
812                let right = format!(
813                    "{old:<METRIC_WIDTH$} ({:^DIFF_WIDTH$})",
814                    WITHIN_TOLERANCE.bright_black()
815                );
816                self.write_field(
817                    field,
818                    &EitherOrBoth::Both(&new.to_string(), &right),
819                    None,
820                    false,
821                );
822            }
823            EitherOrBoth::Both(new, old) => {
824                let diffs = diffs.expect(
825                    "If there are new metrics and old metrics there should be a difference present",
826                );
827                let pct_string = format_float(diffs.diff_pct, '%');
828                let factor_string = format_float(diffs.factor, 'x');
829
830                let right = format!(
831                    "{old:<METRIC_WIDTH$} ({pct_string:^DIFF_WIDTH$}) \
832                     [{factor_string:^DIFF_WIDTH$}]"
833                );
834                self.write_field(
835                    field,
836                    &EitherOrBoth::Both(&new.to_string(), &right),
837                    None,
838                    false,
839                );
840            }
841        }
842    }
843
844    fn write_empty_line(&mut self) {
845        let indent = self.indent.trim_end().to_owned();
846        if !indent.is_empty() {
847            writeln!(self, "{indent}").unwrap();
848        }
849    }
850
851    fn write_left_indented(&mut self, value: &str) {
852        self.write_indent(&IndentKind::Normal);
853        writeln!(self, "{}{value}", " ".repeat(FIELD_WIDTH)).unwrap();
854    }
855
856    /// Format the baseline
857    fn format_baseline(&mut self, baselines: &Baselines) {
858        match baselines {
859            (None, None) => {}
860            (Some(left), Some(right)) if left == right => {
861                let right = format!("{right} (old)");
862                self.write_field("Baselines:", &EitherOrBoth::Both(left, &right), None, false);
863            }
864            _ => {
865                self.write_field(
866                    "Baselines:",
867                    &EitherOrBoth::try_from(baselines.clone())
868                        .expect("At least one baseline should be present")
869                        .as_ref()
870                        .map(String::as_str),
871                    None,
872                    false,
873                );
874            }
875        }
876    }
877
878    fn format_details(&mut self, details: &str) {
879        let mut details = details.lines();
880        if let Some(head_line) = details.next() {
881            self.write_indent(&IndentKind::Normal);
882            writeln!(self, "{:<FIELD_WIDTH$}{}", "Details:", head_line).unwrap();
883            for body_line in details {
884                if body_line.is_empty() {
885                    self.write_empty_line();
886                } else {
887                    self.write_left_indented(body_line);
888                }
889            }
890        }
891    }
892
893    fn format_metrics<'a, K: Display>(
894        &mut self,
895        metrics: impl Iterator<Item = (K, &'a MetricsDiff)>,
896    ) {
897        for (metric_kind, diff) in metrics {
898            let description = format!("{metric_kind}:");
899            self.write_metric(&description, &diff.metrics.as_ref(), diff.diffs);
900        }
901    }
902
903    fn format_tool_total_header(&mut self) {
904        self.write_indent(&IndentKind::ToolSubHeadline);
905        writeln!(self, "{} {}", "##".yellow(), "Total".bold()).unwrap();
906    }
907
908    fn format_multiple_segment_header(&mut self, details: &EitherOrBoth<ProfileInfo>) {
909        fn fields(detail: &ProfileInfo) -> String {
910            let mut result = String::new();
911            write!(result, "pid: {}", detail.pid).unwrap();
912
913            if let Some(ppid) = detail.parent_pid {
914                write!(result, " ppid: {ppid}").unwrap();
915            }
916            if let Some(thread) = detail.thread {
917                write!(result, " thread: {thread}").unwrap();
918            }
919            if let Some(part) = detail.part {
920                write!(result, " part: {part}").unwrap();
921            }
922
923            result
924        }
925
926        self.write_indent(&IndentKind::ToolSubHeadline);
927        write!(self, "{} ", "##".yellow()).unwrap();
928
929        let max_left = LEFT_WIDTH - 3;
930        match details {
931            EitherOrBoth::Left(new) => {
932                let left = fields(new);
933                let len = left.len();
934                let left = left.bold();
935
936                if len > max_left {
937                    writeln!(self, "{left}\n{}|{NOT_AVAILABLE}", " ".repeat(max_left + 5)).unwrap();
938                } else {
939                    writeln!(self, "{left}{}|{NOT_AVAILABLE}", " ".repeat(max_left - len)).unwrap();
940                }
941            }
942            EitherOrBoth::Right(old) => {
943                let right = fields(old);
944
945                writeln!(
946                    self,
947                    "{}{}|{right}",
948                    NOT_AVAILABLE.bold(),
949                    " ".repeat(max_left - NOT_AVAILABLE.len())
950                )
951                .unwrap();
952            }
953            EitherOrBoth::Both(new, old) => {
954                let left = fields(new);
955                let len = left.len();
956                let right = fields(old);
957                let left = left.bold();
958
959                if len > max_left {
960                    writeln!(self, "{left}\n{}|{right}", " ".repeat(max_left + 5)).unwrap();
961                } else {
962                    writeln!(self, "{left}{}|{right}", " ".repeat(max_left - len)).unwrap();
963                }
964            }
965        }
966    }
967
968    fn format_command(&mut self, config: &Config, command: &EitherOrBoth<&String>) {
969        let paths = match command {
970            EitherOrBoth::Left(new) => {
971                if new.starts_with(&config.bench_bin.display().to_string()) {
972                    EitherOrBoth::Left(make_relative(&config.meta.project_root, &config.bench_bin))
973                } else {
974                    EitherOrBoth::Left(make_relative(&config.meta.project_root, PathBuf::from(new)))
975                }
976            }
977            EitherOrBoth::Right(old) => {
978                if old.starts_with(&config.bench_bin.display().to_string()) {
979                    EitherOrBoth::Right(make_relative(&config.meta.project_root, &config.bench_bin))
980                } else {
981                    EitherOrBoth::Right(make_relative(
982                        &config.meta.project_root,
983                        PathBuf::from(old),
984                    ))
985                }
986            }
987            EitherOrBoth::Both(new, old) if new == old => {
988                if new.starts_with(&config.bench_bin.display().to_string()) {
989                    EitherOrBoth::Left(make_relative(&config.meta.project_root, &config.bench_bin))
990                } else {
991                    EitherOrBoth::Left(make_relative(&config.meta.project_root, PathBuf::from(new)))
992                }
993            }
994            EitherOrBoth::Both(new, old) => {
995                let new_command = if new.starts_with(&config.bench_bin.display().to_string()) {
996                    make_relative(&config.meta.project_root, &config.bench_bin)
997                } else {
998                    make_relative(&config.meta.project_root, PathBuf::from(new))
999                };
1000                let old_command = if old.starts_with(&config.bench_bin.display().to_string()) {
1001                    make_relative(&config.meta.project_root, &config.bench_bin)
1002                } else {
1003                    make_relative(&config.meta.project_root, PathBuf::from(old))
1004                };
1005                EitherOrBoth::Both(new_command, old_command)
1006            }
1007        };
1008
1009        self.write_field(
1010            "Command:",
1011            &paths.map(|p| p.display().to_string()),
1012            Some(Color::Blue),
1013            true,
1014        );
1015    }
1016
1017    /// Format the tool headline shown for all tools
1018    pub fn format_tool_headline(&mut self, tool: ValgrindTool) {
1019        self.write_indent(&IndentKind::ToolHeadline);
1020
1021        let id = tool.id();
1022        writeln!(
1023            self,
1024            "{} {} {}",
1025            "=======".bright_black(),
1026            id.to_ascii_uppercase(),
1027            "=".repeat(MAX_WIDTH.saturating_sub(id.len() + 9))
1028                .bright_black(),
1029        )
1030        .unwrap();
1031    }
1032}
1033
1034impl Display for VerticalFormatter {
1035    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1036        f.write_str(&self.buffer)
1037    }
1038}
1039
1040impl Formatter for VerticalFormatter {
1041    fn format_single(
1042        &mut self,
1043        tool: ValgrindTool,
1044        baselines: &Baselines,
1045        info: Option<&EitherOrBoth<ProfileInfo>>,
1046        metrics_summary: &ToolMetricSummary,
1047        is_default_tool: bool,
1048    ) -> Result<()> {
1049        if is_default_tool {
1050            self.format_baseline(baselines);
1051        }
1052
1053        match metrics_summary {
1054            ToolMetricSummary::None => {
1055                if let Some(info) = info {
1056                    if let Some(new) = info.left() {
1057                        if let Some(details) = &new.details {
1058                            self.format_details(details);
1059                        }
1060                    }
1061                }
1062            }
1063            ToolMetricSummary::ErrorTool(summary) => {
1064                let format = match tool {
1065                    ValgrindTool::Memcheck => &self.output_format.memcheck,
1066                    ValgrindTool::Helgrind => &self.output_format.helgrind,
1067                    ValgrindTool::DRD => &self.output_format.drd,
1068                    _ => {
1069                        unreachable!("{tool} should be an error metric tool");
1070                    }
1071                };
1072
1073                self.format_metrics(
1074                    format
1075                        .clone()
1076                        .iter()
1077                        .filter_map(|e| summary.diff_by_kind(e).map(|d| (e, d))),
1078                );
1079
1080                // We only check for `new` errors
1081                if let Some(info) = info {
1082                    if summary
1083                        .diff_by_kind(&ErrorMetric::Errors)
1084                        .is_some_and(|e| e.metrics.left().is_some_and(|l| *l > Metric::Int(0)))
1085                    {
1086                        if let Some(new) = info.left() {
1087                            if let Some(details) = new.details.as_ref() {
1088                                self.format_details(details);
1089                            }
1090                        }
1091                    }
1092                }
1093            }
1094            ToolMetricSummary::Dhat(summary) => self.format_metrics(
1095                self.output_format
1096                    .dhat
1097                    .clone()
1098                    .iter()
1099                    .filter_map(|e| summary.diff_by_kind(e).map(|d| (e, d))),
1100            ),
1101            ToolMetricSummary::Callgrind(summary) => {
1102                self.format_metrics(
1103                    self.output_format
1104                        .callgrind
1105                        .clone()
1106                        .iter()
1107                        .filter_map(|e| summary.diff_by_kind(e).map(|d| (e, d))),
1108                );
1109            }
1110            ToolMetricSummary::Cachegrind(summary) => {
1111                self.format_metrics(
1112                    self.output_format
1113                        .cachegrind
1114                        .clone()
1115                        .iter()
1116                        .filter_map(|e| summary.diff_by_kind(e).map(|d| (e, d))),
1117                );
1118            }
1119        }
1120        Ok(())
1121    }
1122
1123    fn format(
1124        &mut self,
1125        tool: ValgrindTool,
1126        config: &Config,
1127        baselines: &Baselines,
1128        data: &ProfileData,
1129        is_default_tool: bool,
1130    ) -> Result<()> {
1131        if data.has_multiple() && self.output_format.show_intermediate {
1132            let mut first = true;
1133            for part in &data.parts {
1134                self.format_multiple_segment_header(&part.details);
1135                self.format_command(config, &part.details.as_ref().map(|i| &i.command));
1136
1137                if first {
1138                    self.format_single(
1139                        tool,
1140                        baselines,
1141                        Some(&part.details),
1142                        &part.metrics_summary,
1143                        is_default_tool,
1144                    )?;
1145                    first = false;
1146                } else {
1147                    self.format_single(
1148                        tool,
1149                        &(None, None),
1150                        Some(&part.details),
1151                        &part.metrics_summary,
1152                        is_default_tool,
1153                    )?;
1154                }
1155            }
1156
1157            if data.total.is_some() {
1158                self.format_tool_total_header();
1159                self.format_single(
1160                    tool,
1161                    &(None, None),
1162                    None,
1163                    &data.total.summary,
1164                    is_default_tool,
1165                )?;
1166            }
1167        } else if data.total.is_some() {
1168            self.format_single(tool, baselines, None, &data.total.summary, is_default_tool)?;
1169        } else if data.total.is_none() && !data.parts.is_empty() {
1170            // Since there is no total, show_all is partly ignored, and we show all data in a little
1171            // bit more aggregated form without the multiple files headlines. This affects currently
1172            // the output of `Massif` and `BBV`.
1173            for part in &data.parts {
1174                self.format_command(config, &part.details.as_ref().map(|i| &i.command));
1175
1176                if let Some(new) = part.details.left() {
1177                    if let Some(details) = &new.details {
1178                        self.format_details(details);
1179                    }
1180                }
1181            }
1182        } else {
1183            // no data to show
1184        }
1185
1186        Ok(())
1187    }
1188
1189    fn print_comparison(
1190        &mut self,
1191        function_name: &str,
1192        id: &str,
1193        details: Option<&str>,
1194        summaries: Vec<(ValgrindTool, ToolMetricSummary)>,
1195    ) -> Result<()> {
1196        if self.output_format.is_default() {
1197            ComparisonHeader::new(function_name, id, details, &self.output_format).print();
1198
1199            let is_multiple = summaries.len() > 1;
1200            for (tool, summary) in summaries
1201                .iter()
1202                .filter(|(_, s)| *s != ToolMetricSummary::None)
1203            {
1204                if is_multiple || *tool != ValgrindTool::Callgrind {
1205                    self.format_free_form(&format!(
1206                        "{}{} {}\n",
1207                        self.indent_sub_header,
1208                        "-------".bright_black(),
1209                        tool.to_string().to_uppercase()
1210                    ))?;
1211                }
1212                self.format_single(*tool, &(None, None), None, summary, false)?;
1213            }
1214            self.print_buffer();
1215        }
1216
1217        Ok(())
1218    }
1219
1220    fn clear(&mut self) {
1221        self.buffer.clear();
1222    }
1223
1224    fn get_output_format(&self) -> &OutputFormat {
1225        &self.output_format
1226    }
1227
1228    fn format_free_form(&mut self, line: &str) -> Result<()> {
1229        self.buffer.push_str(line);
1230        Ok(())
1231    }
1232}
1233
1234impl Write for VerticalFormatter {
1235    fn write_str(&mut self, s: &str) -> std::fmt::Result {
1236        self.buffer.push_str(s);
1237        Ok(())
1238    }
1239}
1240
1241/// Format a floating point number with `unit`
1242pub fn format_float(float: f64, unit: char) -> ColoredString {
1243    let signed_short = to_string_signed_short(float);
1244    if float.is_infinite() {
1245        if float.is_sign_positive() {
1246            format!("{signed_short:+^DIFF_WIDTH$}").bright_red().bold()
1247        } else {
1248            format!("{signed_short:-^DIFF_WIDTH$}")
1249                .bright_green()
1250                .bold()
1251        }
1252    } else if float.is_sign_positive() {
1253        format!("{signed_short:>+FLOAT_WIDTH$}{unit}")
1254            .bright_red()
1255            .bold()
1256    } else {
1257        format!("{signed_short:>+FLOAT_WIDTH$}{unit}")
1258            .bright_green()
1259            .bold()
1260    }
1261}
1262
1263/// Return the formatted string if `NoCapture` is not `False`
1264pub fn no_capture_footer(nocapture: NoCapture) -> Option<String> {
1265    match nocapture {
1266        NoCapture::True => Some(format!(
1267            "{} {}",
1268            "-".yellow(),
1269            "end of stdout/stderr".yellow()
1270        )),
1271        NoCapture::False => None,
1272        NoCapture::Stderr => Some(format!("{} {}", "-".yellow(), "end of stderr".yellow())),
1273        NoCapture::Stdout => Some(format!("{} {}", "-".yellow(), "end of stdout".yellow())),
1274    }
1275}
1276
1277/// Print the summary of the --list argument
1278pub fn print_benchmark_list_summary(sum: u64) {
1279    if sum != 0 {
1280        println!();
1281    }
1282    println!("0 tests, {sum} benchmarks");
1283}
1284
1285/// Print a single benchmark for the --list argument
1286pub fn print_list_benchmark(module_path: &ModulePath, id: Option<&String>) {
1287    match id {
1288        Some(id) => {
1289            println!("{module_path}::{id}: benchmark");
1290        }
1291        None => {
1292            println!("{module_path}: benchmark");
1293        }
1294    }
1295}
1296
1297/// Print the appropriate footer for the [`NoCapture`] option
1298pub fn print_no_capture_footer(
1299    nocapture: NoCapture,
1300    stdout: Option<&api::Stdio>,
1301    stderr: Option<&api::Stdio>,
1302) {
1303    let stdout_is_pipe = stdout.map_or(
1304        nocapture == NoCapture::False || nocapture == NoCapture::Stderr,
1305        api::Stdio::is_pipe,
1306    );
1307
1308    let stderr_is_pipe = stderr.map_or(
1309        nocapture == NoCapture::False || nocapture == NoCapture::Stdout,
1310        api::Stdio::is_pipe,
1311    );
1312
1313    // These unwraps are safe because `no_capture_footer` returns None only if `NoCapture` is
1314    // `False`
1315    match (stdout_is_pipe, stderr_is_pipe) {
1316        (true, true) => {}
1317        (true, false) => {
1318            println!("{}", no_capture_footer(NoCapture::Stderr).unwrap());
1319        }
1320        (false, true) => {
1321            println!("{}", no_capture_footer(NoCapture::Stdout).unwrap());
1322        }
1323        (false, false) => {
1324            println!("{}", no_capture_footer(NoCapture::True).unwrap());
1325        }
1326    }
1327}
1328
1329/// Print detected regressions to `stderr`
1330pub fn print_regressions(regressions: &[ToolRegression]) {
1331    for regression in regressions {
1332        match regression {
1333            ToolRegression::Soft {
1334                metric,
1335                new,
1336                old,
1337                diff_pct,
1338                limit,
1339            } => {
1340                let metric_name = match metric {
1341                    MetricKind::None => continue,
1342                    MetricKind::Callgrind(event_kind) => event_kind.to_string(),
1343                    MetricKind::Cachegrind(cachegrind_metric) => cachegrind_metric.to_string(),
1344                    MetricKind::Dhat(dhat_metric) => dhat_metric.to_string(),
1345                    MetricKind::Memcheck(error_metric)
1346                    | MetricKind::Helgrind(error_metric)
1347                    | MetricKind::DRD(error_metric) => error_metric.to_string(),
1348                };
1349
1350                if limit.is_sign_positive() {
1351                    eprintln!(
1352                        "Performance has {0}: {1} ({old} -> {2}) regressed by {3:>+6} (>{4:>+6})",
1353                        "regressed".bold().bright_red(),
1354                        metric_name,
1355                        new.to_string().bold(),
1356                        format!("{}%", to_string_signed_short(*diff_pct))
1357                            .bold()
1358                            .bright_red(),
1359                        format!("{}%", to_string_signed_short(*limit)).bright_black()
1360                    );
1361                } else {
1362                    eprintln!(
1363                        "Performance has {0}: {1} ({old} -> {2}) regressed by {3:>+6} (<{4:>+6})",
1364                        "regressed".bold().bright_red(),
1365                        metric_name,
1366                        new.to_string().bold(),
1367                        format!("{}%", to_string_signed_short(*diff_pct))
1368                            .bold()
1369                            .bright_red(),
1370                        format!("{}%", to_string_signed_short(*limit)).bright_black()
1371                    );
1372                }
1373            }
1374            ToolRegression::Hard {
1375                metric,
1376                new,
1377                diff,
1378                limit,
1379            } => {
1380                let metric_name = match metric {
1381                    MetricKind::None => continue,
1382                    MetricKind::Callgrind(event_kind) => event_kind.to_string(),
1383                    MetricKind::Cachegrind(cachegrind_metric) => cachegrind_metric.to_string(),
1384                    MetricKind::Dhat(dhat_metric) => dhat_metric.to_string(),
1385                    MetricKind::Memcheck(error_metric)
1386                    | MetricKind::Helgrind(error_metric)
1387                    | MetricKind::DRD(error_metric) => error_metric.to_string(),
1388                };
1389
1390                eprintln!(
1391                    "Performance has {0}: {1} ({2}) exceeds limit by {3} (>{4})",
1392                    "regressed".bold().bright_red(),
1393                    metric_name,
1394                    new.to_string().bold(),
1395                    diff.to_string().bold().bright_red(),
1396                    limit.to_string().bright_black(),
1397                );
1398            }
1399        }
1400    }
1401}
1402
1403fn truncate_description(description: &str, truncate_description: Option<usize>) -> Cow<'_, str> {
1404    if let Some(num) = truncate_description {
1405        let new_description = truncate_str_utf8(description, num);
1406        if new_description.len() < description.len() {
1407            Cow::Owned(format!("{new_description}..."))
1408        } else {
1409            Cow::Borrowed(description)
1410        }
1411    } else {
1412        Cow::Borrowed(description)
1413    }
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418    use indexmap::indexmap;
1419    use pretty_assertions::assert_eq;
1420    use rstest::rstest;
1421
1422    use super::*;
1423    use crate::runner::metrics::{Metrics, MetricsSummary};
1424
1425    #[rstest]
1426    #[case::simple("some::module", Some("id"), Some("1, 2"), "some::module id:1, 2")]
1427    #[case::id_but_no_description("some::module", Some("id"), None, "some::module id")]
1428    #[case::id_but_empty_description("some::module", Some("id"), Some(""), "some::module id")]
1429    #[case::no_id_but_description("some::module", None, Some("1, 2, 3"), "some::module 1, 2, 3")]
1430    #[case::no_id_no_description("some::module", None, None, "some::module")]
1431    #[case::no_id_empty_description("some::module", None, Some(""), "some::module")]
1432    #[case::length_is_greater_than_default(
1433        "some::module",
1434        Some("id"),
1435        Some("012345678901234567890123456789012345678901234567890123456789"),
1436        "some::module id:012345678901234567890123456789012345678901234567890123456789"
1437    )]
1438    fn test_header_display_when_no_truncate(
1439        #[case] module_path: &str,
1440        #[case] id: Option<&str>,
1441        #[case] description: Option<&str>,
1442        #[case] expected: &str,
1443    ) {
1444        colored::control::set_override(false);
1445
1446        let output_format = OutputFormat {
1447            truncate_description: None,
1448            ..Default::default()
1449        };
1450        let header = Header::new(
1451            &ModulePath::new(module_path),
1452            id.map(ToOwned::to_owned),
1453            description.map(ToOwned::to_owned),
1454            &output_format,
1455        );
1456
1457        assert_eq!(header.to_string(), expected);
1458    }
1459
1460    #[rstest]
1461    #[case::truncate_0(
1462        "some::module",
1463        Some("id"),
1464        Some("1, 2, 3"),
1465        Some(0),
1466        "some::module id:..."
1467    )]
1468    #[case::truncate_0_when_length_is_0(
1469        "some::module",
1470        Some("id"),
1471        Some(""),
1472        Some(0),
1473        "some::module id"
1474    )]
1475    #[case::truncate_0_when_length_is_1(
1476        "some::module",
1477        Some("id"),
1478        Some("1"),
1479        Some(0),
1480        "some::module id:..."
1481    )]
1482    #[case::truncate_1(
1483        "some::module",
1484        Some("id"),
1485        Some("1, 2, 3"),
1486        Some(1),
1487        "some::module id:1..."
1488    )]
1489    #[case::truncate_1_when_length_is_0(
1490        "some::module",
1491        Some("id"),
1492        Some(""),
1493        Some(1),
1494        "some::module id"
1495    )]
1496    #[case::truncate_1_when_length_is_1(
1497        "some::module",
1498        Some("id"),
1499        Some("1"),
1500        Some(1),
1501        "some::module id:1"
1502    )]
1503    #[case::truncate_1_when_length_is_2(
1504        "some::module",
1505        Some("id"),
1506        Some("1,"),
1507        Some(1),
1508        "some::module id:1..."
1509    )]
1510    #[case::truncate_3(
1511        "some::module",
1512        Some("id"),
1513        Some("1, 2, 3"),
1514        Some(3),
1515        "some::module id:1, ..."
1516    )]
1517    #[case::truncate_3_when_length_is_2(
1518        "some::module",
1519        Some("id"),
1520        Some("1,"),
1521        Some(3),
1522        "some::module id:1,"
1523    )]
1524    #[case::truncate_3_when_length_is_3(
1525        "some::module",
1526        Some("id"),
1527        Some("1, "),
1528        Some(3),
1529        "some::module id:1, "
1530    )]
1531    #[case::truncate_3_when_length_is_4(
1532        "some::module",
1533        Some("id"),
1534        Some("1, 2"),
1535        Some(3),
1536        "some::module id:1, ..."
1537    )]
1538    #[case::truncate_is_smaller_than_length(
1539        "some::module",
1540        Some("id"),
1541        Some("1, 2, 3, 4, 5"),
1542        Some(4),
1543        "some::module id:1, 2..."
1544    )]
1545    #[case::truncate_is_one_smaller_than_length(
1546        "some::module",
1547        Some("id"),
1548        Some("1, 2, 3"),
1549        Some(6),
1550        "some::module id:1, 2, ..."
1551    )]
1552    #[case::truncate_is_one_greater_than_length(
1553        "some::module",
1554        Some("id"),
1555        Some("1, 2, 3"),
1556        Some(8),
1557        "some::module id:1, 2, 3"
1558    )]
1559    #[case::truncate_is_far_greater_than_length(
1560        "some::module",
1561        Some("id"),
1562        Some("1, 2, 3"),
1563        Some(100),
1564        "some::module id:1, 2, 3"
1565    )]
1566    #[case::truncate_is_equal_to_length(
1567        "some::module",
1568        Some("id"),
1569        Some("1, 2, 3"),
1570        Some(7),
1571        "some::module id:1, 2, 3"
1572    )]
1573    #[case::description_is_empty(
1574        "some::module",
1575        Some("id"),
1576        Some(""),
1577        Some(100),
1578        "some::module id"
1579    )]
1580    fn test_header_display_when_truncate(
1581        #[case] module_path: &str,
1582        #[case] id: Option<&str>,
1583        #[case] description: Option<&str>,
1584        #[case] truncate_description: Option<usize>,
1585        #[case] expected: &str,
1586    ) {
1587        colored::control::set_override(false);
1588
1589        let output_format = OutputFormat {
1590            truncate_description,
1591            ..Default::default()
1592        };
1593
1594        let header = Header::new(
1595            &ModulePath::new(module_path),
1596            id.map(ToOwned::to_owned),
1597            description.map(ToOwned::to_owned),
1598            &output_format,
1599        );
1600
1601        assert_eq!(header.to_string(), expected);
1602    }
1603
1604    #[rstest]
1605    #[case::new_costs_0(EventKind::Ir, 0, None, "*********", None)]
1606    #[case::old_costs_0(EventKind::Ir, 1, Some(0), "+++inf+++", Some("+++inf+++"))]
1607    #[case::all_costs_0(EventKind::Ir, 0, Some(0), "No change", None)]
1608    #[case::new_costs_u64_max(EventKind::Ir, u64::MAX, None, "*********", None)]
1609    #[case::old_costs_u64_max(EventKind::Ir, u64::MAX / 10, Some(u64::MAX), "-90.0000%", Some("-10.0000x"))]
1610    #[case::all_costs_u64_max(EventKind::Ir, u64::MAX, Some(u64::MAX), "No change", None)]
1611    #[case::no_change_when_not_0(EventKind::Ir, 1000, Some(1000), "No change", None)]
1612    #[case::neg_change_when_not_0(EventKind::Ir, 2000, Some(3000), "-33.3333%", Some("-1.50000x"))]
1613    #[case::pos_change_when_not_0(EventKind::Ir, 2000, Some(1000), "+100.000%", Some("+2.00000x"))]
1614    #[case::pos_inf(EventKind::Ir, 2000, Some(0), "+++inf+++", Some("+++inf+++"))]
1615    #[case::neg_inf(EventKind::Ir, 0, Some(2000), "-100.000%", Some("---inf---"))]
1616    fn test_format_vertical_when_new_costs_are_present(
1617        #[case] event_kind: EventKind,
1618        #[case] new: u64,
1619        #[case] old: Option<u64>,
1620        #[case] diff_pct: &str,
1621        #[case] diff_fact: Option<&str>,
1622    ) {
1623        colored::control::set_override(false);
1624
1625        let costs = match old {
1626            Some(old) => EitherOrBoth::Both(
1627                Metrics(indexmap! {event_kind => Metric::Int(new)}),
1628                Metrics(indexmap! {event_kind => Metric::Int(old)}),
1629            ),
1630            None => EitherOrBoth::Left(Metrics(indexmap! {event_kind => Metric::Int(new)})),
1631        };
1632        let metrics_summary = MetricsSummary::new(costs);
1633        let mut formatter = VerticalFormatter::new(OutputFormat::default());
1634        formatter.format_metrics(metrics_summary.all_diffs());
1635
1636        let expected = format!(
1637            "  {:<21}{new:>METRIC_WIDTH$}|{:<METRIC_WIDTH$} ({diff_pct}){}\n",
1638            format!("{event_kind}:"),
1639            old.map_or(NOT_AVAILABLE.to_owned(), |o| o.to_string()),
1640            diff_fact.map_or_else(String::new, |f| format!(" [{f}]"))
1641        );
1642
1643        assert_eq!(formatter.buffer, expected);
1644    }
1645
1646    #[rstest]
1647    #[case::no_change(2000, Some(2000), 50.0, "No change", None)]
1648    #[case::new_costs_0_no_old(0, None, 50.0, "*********", None)]
1649    #[case::old_costs_0(1, Some(0), 50.0, "+++inf+++", Some("+++inf+++"))]
1650    #[case::all_costs_0(0, Some(0), 50.0, "No change", None)]
1651    #[case::all_0(0, Some(0), 0.0, "No change", None)]
1652    #[case::neg_change_when_tolerance_0(2000, Some(3000), 0.0, "-33.3333%", Some("-1.50000x"))]
1653    #[case::pos_change_when_tolerance_0(2000, Some(1000), 0.0, "+100.000%", Some("+2.00000x"))]
1654    #[case::neg_change_when_within_tolerance(2000, Some(3000), 50.0, "Tolerance", None)]
1655    #[case::neg_change_when_within_tolerance_exact(
1656        2000,
1657        Some(3000),
1658        1.0 / 3.0 * 100.0,
1659        "Tolerance",
1660        None
1661    )]
1662    #[case::pos_change_when_within_tolerance(3000, Some(2000), 50.0, "Tolerance", None)]
1663    #[case::pos_change_when_neg_tolerance(3000, Some(2000), -50.0, "Tolerance", None)]
1664    #[case::pos_change_when_tolerance_is_nan(
1665        2000,
1666        Some(1000),
1667        f64::NAN,
1668        "+100.000%",
1669        Some("+2.00000x")
1670    )]
1671    fn test_format_vertical_when_tolerance_is_set(
1672        #[case] new: u64,
1673        #[case] old: Option<u64>,
1674        #[case] tolerance: f64,
1675        #[case] diff_pct: &str,
1676        #[case] diff_fact: Option<&str>,
1677    ) {
1678        colored::control::set_override(false);
1679
1680        let expected = format!(
1681            "  {:<FIELD_WIDTH$}{new:>METRIC_WIDTH$}|{:<METRIC_WIDTH$} ({diff_pct}){}\n",
1682            format!("{}:", EventKind::Ir),
1683            old.map_or(NOT_AVAILABLE.to_owned(), |o| o.to_string()),
1684            diff_fact.map_or_else(String::new, |f| format!(" [{f}]"))
1685        );
1686
1687        let output_format = OutputFormat {
1688            tolerance: Some(tolerance),
1689            ..Default::default()
1690        };
1691
1692        let costs = match old {
1693            Some(old) => EitherOrBoth::Both(
1694                Metrics(indexmap! {EventKind::Ir => Metric::Int(new)}),
1695                Metrics(indexmap! {EventKind::Ir => Metric::Int(old)}),
1696            ),
1697            None => EitherOrBoth::Left(Metrics(indexmap! {EventKind::Ir => Metric::Int(new)})),
1698        };
1699        let metrics_summary = MetricsSummary::new(costs);
1700        let mut formatter = VerticalFormatter::new(output_format);
1701        formatter.format_metrics(metrics_summary.all_diffs());
1702
1703        assert_eq!(formatter.buffer, expected);
1704    }
1705
1706    #[rstest]
1707    #[case::normal_no_grid(IndentKind::Normal, false, "  ")]
1708    #[case::tool_header_no_grid(IndentKind::ToolHeadline, false, "  ")]
1709    #[case::tool_sub_header_no_grid(IndentKind::ToolSubHeadline, false, "  ")]
1710    #[case::normal_with_grid(IndentKind::Normal, true, "| ")]
1711    #[case::tool_header_with_grid(IndentKind::ToolHeadline, true, "|=")]
1712    #[case::tool_sub_header_with_grid(IndentKind::ToolSubHeadline, true, "|-")]
1713    fn test_vertical_formatter_write_indent(
1714        #[case] kind: IndentKind,
1715        #[case] show_grid: bool,
1716        #[case] expected: &str,
1717    ) {
1718        colored::control::set_override(false);
1719
1720        let output_format = OutputFormat {
1721            show_grid,
1722            ..Default::default()
1723        };
1724
1725        let mut formatter = VerticalFormatter::new(output_format);
1726        formatter.write_indent(&kind);
1727        assert_eq!(formatter.buffer, expected);
1728    }
1729
1730    #[rstest]
1731    #[case::left(
1732        "Some:",
1733        EitherOrBoth::Left("left"),
1734        "  Some:                                left\n"
1735    )]
1736    #[case::right(
1737        "Field:",
1738        EitherOrBoth::Right("right"),
1739        "  Field:                                   |right\n"
1740    )]
1741    #[case::both(
1742        "Field:",
1743        EitherOrBoth::Both("left", "right"),
1744        "  Field:                               left|right\n"
1745    )]
1746    #[case::both_u64_max(
1747        "Field:",
1748        EitherOrBoth::Both(format!("{}", u64::MAX), format!("{}", u64::MAX)),
1749        "  Field:               18446744073709551615|18446744073709551615\n"
1750    )]
1751    #[case::split(
1752        "Field:",
1753        EitherOrBoth::Both(format!("{}1", u64::MAX), "right".to_owned()),
1754        "  Field:               184467440737095516151\n                                           |right\n"
1755    )]
1756    fn test_vertical_formatter_write_field<T>(
1757        #[case] field: &str,
1758        #[case] values: EitherOrBoth<T>,
1759        #[case] expected: &str,
1760    ) where
1761        T: AsRef<str>,
1762    {
1763        colored::control::set_override(false);
1764
1765        let output_format = OutputFormat::default();
1766
1767        let mut formatter = VerticalFormatter::new(output_format);
1768        formatter.write_field(field, &values, None, false);
1769        assert_eq!(formatter.buffer, expected);
1770    }
1771}