Skip to main content

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}