criterion_markdown/lib.rs
1//! Reads criterion benchmark results from `target/criterion/` JSON files and
2//! renders a markdown table similar to criterion-table.
3//!
4//! # Example
5//!
6//! ```rust,no_run
7//! fn main() -> anyhow::Result<()> {
8//! let markdown = criterion_markdown::render("target/criterion", std::iter::empty::<&str>())?;
9//! println!("{markdown}");
10//! Ok(())
11//! }
12//! ```
13//!
14//! # Collapsible output
15//!
16//! Use [`RenderOptions::collapsible`] to wrap the output in a
17//! `<details><summary>` tag:
18//!
19//! ```rust,no_run
20//! use criterion_markdown::RenderOptions;
21//!
22//! fn main() -> anyhow::Result<()> {
23//! let options = RenderOptions {
24//! collapsible: Some("Benchmark Results".into()),
25//! };
26//! let markdown = criterion_markdown::render_with_options(
27//! "target/criterion",
28//! std::iter::empty::<&str>(),
29//! &options,
30//! )?;
31//! println!("{markdown}");
32//! Ok(())
33//! }
34//! ```
35
36use std::path::Path;
37
38use anyhow::Result;
39
40mod discovery;
41mod markdown;
42mod model;
43
44/// Options for controlling the rendered markdown output.
45#[derive(Debug, Clone, Default)]
46pub struct RenderOptions {
47 /// If set, wraps the output in a `<details><summary>...</summary>` tag
48 /// using this value as the summary text.
49 pub collapsible: Option<String>,
50}
51
52/// Reads all benchmark results from the given criterion output directory
53/// and renders a markdown table.
54///
55/// `allowlist` filters benchmarks by `full_id`.
56///
57/// If the iterator is empty, no filtering is applied.
58pub fn render(
59 criterion_dir: impl AsRef<Path>,
60 allowlist: impl IntoIterator<Item = impl AsRef<str>>,
61) -> Result<String> {
62 render_with_options(criterion_dir, allowlist, &RenderOptions::default())
63}
64
65/// Like [`render`], but accepts additional [`RenderOptions`] to control output.
66pub fn render_with_options(
67 criterion_dir: impl AsRef<Path>,
68 allowlist: impl IntoIterator<Item = impl AsRef<str>>,
69 options: &RenderOptions,
70) -> Result<String> {
71 let criterion_dir = criterion_dir.as_ref();
72 let mut entries = discovery::discover_benchmarks(criterion_dir)?;
73 let names: Vec<String> = allowlist
74 .into_iter()
75 .map(|n| n.as_ref().to_string())
76 .collect();
77 if !names.is_empty() {
78 entries.retain(|e| names.iter().any(|n| n == &e.full_id));
79 }
80 if entries.is_empty() {
81 anyhow::bail!("No benchmark results found in {}", criterion_dir.display());
82 }
83 let skip_headers = options.collapsible.is_some();
84 let body = markdown::format_table(&entries, skip_headers);
85 Ok(match &options.collapsible {
86 Some(summary) => {
87 let range = markdown::compute_summary(&entries)
88 .map(|info| format!(" ({} → {})", info.worst_change, info.best_change))
89 .unwrap_or_default();
90 format!("<details>\n<summary>{summary}{range}</summary>\n\n{body}\n</details>\n")
91 }
92 None => body,
93 })
94}