1use 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
29pub const DIFF_WIDTH: usize = 9;
31pub const FIELD_WIDTH: usize = 21;
33pub const FLOAT_WIDTH: usize = DIFF_WIDTH - 1;
35pub const LEFT_WIDTH: usize = METRIC_WIDTH + FIELD_WIDTH;
37#[allow(clippy::doc_link_with_quotes)]
38pub const MAX_WIDTH: usize = 2 + LEFT_WIDTH + 1 + METRIC_WIDTH + 2 * 11;
42pub const METRIC_WIDTH: usize = 20;
44pub const NOT_AVAILABLE: &str = "N/A";
46pub const NO_CHANGE: &str = "No change";
48pub const UNKNOWN: &str = "*********";
50pub const WITHIN_TOLERANCE: &str = "Tolerance";
52
53enum IndentKind {
54 Normal,
55 ToolHeadline,
56 ToolSubHeadline,
57}
58
59#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
61pub enum OutputFormatKind {
62 #[default]
64 Default,
65 Json,
67 PrettyJson,
69}
70
71pub struct BinaryBenchmarkHeader {
75 inner: Header,
76 output_format: OutputFormat,
77}
78
79pub struct ComparisonHeader {
81 pub details: Option<String>,
83 pub function_name: String,
85 pub id: String,
87 pub indent: String,
89}
90
91pub struct Header {
93 description: Option<String>,
94 id: Option<String>,
95 module_path: String,
96}
97pub struct LibraryBenchmarkHeader {
101 inner: Header,
102 output_format: OutputFormat,
103}
104
105#[derive(Debug, Clone, PartialEq)]
107pub struct OutputFormat {
108 pub cachegrind: IndexSet<CachegrindMetric>,
110 pub callgrind: IndexSet<EventKind>,
112 pub dhat: IndexSet<DhatMetric>,
114 pub drd: IndexSet<ErrorMetric>,
116 pub helgrind: IndexSet<ErrorMetric>,
118 pub kind: OutputFormatKind,
120 pub memcheck: IndexSet<ErrorMetric>,
122 pub show_grid: bool,
124 pub show_intermediate: bool,
126 pub tolerance: Option<f64>,
128 pub truncate_description: Option<usize>,
130}
131
132#[derive(Debug, Clone)]
134pub struct SummaryFormatter {
135 pub output_format_kind: OutputFormatKind,
137}
138
139#[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
149pub trait Formatter {
151 fn clear(&mut self);
153
154 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 fn format_free_form(&mut self, line: &str) -> Result<()>;
167
168 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 fn get_output_format(&self) -> &OutputFormat;
180
181 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 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 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 pub fn print(&self) {
252 if self.output_format.kind == OutputFormatKind::Default {
253 self.inner.print();
254 }
255 }
256
257 pub fn to_title(&self) -> String {
259 self.inner.to_title()
260 }
261
262 pub fn description(&self) -> Option<String> {
264 self.inner.description.clone()
265 }
266}
267
268impl ComparisonHeader {
269 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 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 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 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 pub fn print(&self) {
353 println!("{self}");
354 }
355
356 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 }
401 Ok(())
402 }
403}
404
405impl LibraryBenchmarkHeader {
406 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 pub fn print(&self) {
423 if self.output_format.is_default() {
424 self.inner.print();
425 }
426 }
427
428 pub fn to_title(&self) -> String {
430 self.inner.to_title()
431 }
432
433 pub fn description(&self) -> Option<String> {
435 self.inner.description.clone()
436 }
437}
438
439impl OutputFormat {
440 pub fn is_default(&self) -> bool {
442 self.kind == OutputFormatKind::Default
443 }
444
445 pub fn is_json(&self) -> bool {
447 self.kind == OutputFormatKind::Json || self.kind == OutputFormatKind::PrettyJson
448 }
449
450 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 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 pub fn new(output_format_kind: OutputFormatKind) -> Self {
561 Self { output_format_kind }
562 }
563
564 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 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 pub fn print_buffer(&mut self) {
668 print!("{}", self.buffer);
669 self.clear();
670 }
671
672 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 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 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 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 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 }
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
1241pub 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
1263pub 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
1277pub fn print_benchmark_list_summary(sum: u64) {
1279 if sum != 0 {
1280 println!();
1281 }
1282 println!("0 tests, {sum} benchmarks");
1283}
1284
1285pub 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
1297pub 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 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
1329pub 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}