Skip to content

Commit

Permalink
feat: report check and diff in markdown (#126)
Browse files Browse the repository at this point in the history
* feat: create html table without details

Signed-off-by: Jérémie Drouet <[email protected]>

* feat: create markdown check format

Signed-off-by: Jérémie Drouet <[email protected]>

* feat: display warning for diff formatted in markdown

Signed-off-by: Jérémie Drouet <[email protected]>

* feat: implement diff markdown format

Signed-off-by: Jérémie Drouet <[email protected]>

* style: apply clippy suggestions

Signed-off-by: Jérémie Drouet <[email protected]>

* style: format code

Signed-off-by: Jérémie Drouet <[email protected]>

* build: update dependencies

Signed-off-by: Jérémie Drouet <[email protected]>

---------

Signed-off-by: Jérémie Drouet <[email protected]>
  • Loading branch information
jdrouet authored Dec 10, 2024
1 parent 7a2190a commit 7f29875
Show file tree
Hide file tree
Showing 32 changed files with 1,073 additions and 285 deletions.
495 changes: 371 additions & 124 deletions Cargo.lock

Large diffs are not rendered by default.

37 changes: 18 additions & 19 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,23 @@ impl-command = []
impl-git2 = ["dep:git2", "dep:auth-git2"]

[dependencies]
auth-git2 = { version = "0.5.5", optional = true, features = ["log"] }
clap = { version = "4.5.21", features = ["derive", "env"] }
git2 = { version = "0.19.0", optional = true }
human-number = { version = "0.1.3" }
indexmap = { version = "2.7.0", features = ["serde"] }
lcov = { version = "0.8.1", optional = true }
nu-ansi-term = { version = "0.50.1" }
serde = { version = "1.0.215", features = ["derive"] }
serde_json = { version = "1.0.133", features = [
"preserve_order",
], optional = true }
thiserror = { version = "2.0.4" }
toml = { version = "0.8.19", features = ["preserve_order"] }
tracing = { version = "0.1.41" }
tracing-subscriber = { version = "0.3.19" }
another-html-builder = "0.2"
auth-git2 = { version = "0.5", optional = true, features = ["log"] }
clap = { version = "4.5", features = ["derive", "env"] }
git2 = { version = "0.19", optional = true }
human-number = { version = "0.1" }
indexmap = { version = "2.7", features = ["serde"] }
lcov = { version = "0.8", optional = true }
nu-ansi-term = { version = "0.50" }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["preserve_order"], optional = true }
thiserror = { version = "2.0" }
toml = { version = "0.8", features = ["preserve_order"] }
tracing = { version = "0.1" }
tracing-subscriber = { version = "0.3" }

[dev-dependencies]
mockall = "0.13.1"
similar-asserts = "1.6.0"
tempfile = "3.14.0"
test-case = "3.3.1"
mockall = "0.13"
similar-asserts = "1.6"
tempfile = "3.14"
test-case = "3.3"
2 changes: 1 addition & 1 deletion src/cmd/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl super::Executor for CommandAdd {
fn execute<B: Backend, Out: PrettyWriter>(
self,
backend: B,
_stdout: &mut Out,
_stdout: Out,
) -> Result<ExitCode, crate::service::Error> {
let metric = crate::entity::metric::Metric {
header: crate::entity::metric::MetricHeader {
Expand Down
1 change: 1 addition & 0 deletions src/cmd/check/format/format_md_by_default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<table><thead><tr><th align="center">Status</th><th align="left">Metric</th><th align="right">Previous value</th><th align="right">Current value</th><th align="right">Change</th></tr></thead><tbody><tr><td align="center">⛔️</td><td align="left">first{platform.os="linux", platform.arch="amd64", unit="byte"}</td><td align="right">10.00</td><td align="right">20.00</td><td align="right">10.00<br />(+100.00 %)</td></tr><tr><td></td><td colspan="4"><i>show_not_increase_too_much</i><br />⛔️ increase should be less than 20.00 %<br /></td></tr><tr><td align="center">✅</td><td align="left">first{platform.os="linux", platform.arch="arm64", unit="byte"}</td><td align="right">10.00</td><td align="right">11.00</td><td align="right">1.00<br />(+10.00 %)</td></tr><tr><td align="center">⏭️</td><td align="left">unknown</td><td align="right">42.00</td><td align="right">28.00</td><td align="right">-14.00<br />(-33.33 %)</td></tr><tr><td align="center">⏭️</td><td align="left">noglobal</td><td align="right">42.00</td><td align="right">28.00</td><td align="right">-14.00<br />(-33.33 %)</td></tr><tr><td align="center">✅</td><td align="left">nochange</td><td align="right">10.00</td><td align="right">10.00</td><td align="right">0.00<br />(+0.00 %)</td></tr><tr><td align="center">✅</td><td align="left">with-unit</td><td align="right">20.00 MiB</td><td align="right">25.00 MiB</td><td align="right">5.00 MiB<br />(+25.00 %)</td></tr><tr><td align="center">⛔️</td><td align="left">with-change</td><td align="right">20971520.00</td><td align="right">26214400.00</td><td align="right">5242880.00<br />(+25.00 %)</td></tr><tr><td></td><td colspan="4">⛔️ increase should be less than 2097152.00<br /></td></tr></tbody></table>
1 change: 1 addition & 0 deletions src/cmd/check/format/format_md_with_success_showed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<table><thead><tr><th align="center">Status</th><th align="left">Metric</th><th align="right">Previous value</th><th align="right">Current value</th><th align="right">Change</th></tr></thead><tbody><tr><td align="center">⛔️</td><td align="left">first{platform.os="linux", platform.arch="amd64", unit="byte"}</td><td align="right">10.00</td><td align="right">20.00</td><td align="right">10.00<br />(+100.00 %)</td></tr><tr><td></td><td colspan="4">✅ should be lower than 30.00<br /><i>show_not_increase_too_much</i><br />⛔️ increase should be less than 20.00 %<br /></td></tr><tr><td align="center">✅</td><td align="left">first{platform.os="linux", platform.arch="arm64", unit="byte"}</td><td align="right">10.00</td><td align="right">11.00</td><td align="right">1.00<br />(+10.00 %)</td></tr><tr><td></td><td colspan="4">✅ should be lower than 30.00<br /><i>show_not_increase_too_much</i><br />✅ increase should be less than 20.00 %<br /></td></tr><tr><td align="center">⏭️</td><td align="left">unknown</td><td align="right">42.00</td><td align="right">28.00</td><td align="right">-14.00<br />(-33.33 %)</td></tr><tr><td align="center">⏭️</td><td align="left">noglobal</td><td align="right">42.00</td><td align="right">28.00</td><td align="right">-14.00<br />(-33.33 %)</td></tr><tr><td></td><td colspan="4"><i>show_pass</i><br />⏭️ increase should be less than 20.00 %<br /></td></tr><tr><td align="center">✅</td><td align="left">nochange</td><td align="right">10.00</td><td align="right">10.00</td><td align="right">0.00<br />(+0.00 %)</td></tr><tr><td></td><td colspan="4">✅ should be lower than 30.00<br /></td></tr><tr><td align="center">✅</td><td align="left">with-unit</td><td align="right">20.00 MiB</td><td align="right">25.00 MiB</td><td align="right">5.00 MiB<br />(+25.00 %)</td></tr><tr><td></td><td colspan="4">✅ should be lower than 30.00 MiB<br /></td></tr><tr><td align="center">⛔️</td><td align="left">with-change</td><td align="right">20971520.00</td><td align="right">26214400.00</td><td align="right">5242880.00<br />(+25.00 %)</td></tr><tr><td></td><td colspan="4">✅ increase should be less than 10485760.00<br />⛔️ increase should be less than 2097152.00<br /></td></tr></tbody></table>
File renamed without changes.
174 changes: 174 additions & 0 deletions src/cmd/check/format/html.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
use another_html_builder::prelude::WriterExt;
use another_html_builder::{Body, Buffer};

use crate::entity::check::{MetricCheck, RuleCheck, StatusCount};
use crate::entity::config::Config;
use crate::formatter::metric::TextMetricHeader;
use crate::formatter::percent::TextPercent;
use crate::formatter::rule::TextRule;

fn empty<W: WriterExt>(buf: Buffer<W, Body<'_>>) -> Buffer<W, Body<'_>> {
buf
}

fn text<W: WriterExt>(
value: &'static str,
) -> impl FnOnce(Buffer<W, Body<'_>>) -> Buffer<W, Body<'_>> {
|buf: Buffer<W, Body<'_>>| buf.text(value)
}

fn write_thead<W: WriterExt>(buf: Buffer<W, Body<'_>>) -> Buffer<W, Body<'_>> {
buf.node("thead").content(|buf| {
buf.node("tr").content(|buf| {
buf.node("th")
.attr(("align", "center"))
.content(text("Status"))
.node("th")
.attr(("align", "left"))
.content(text("Metric"))
.node("th")
.attr(("align", "right"))
.content(text("Previous value"))
.node("th")
.attr(("align", "right"))
.content(text("Current value"))
.node("th")
.attr(("align", "right"))
.content(text("Change"))
})
})
}

fn should_display_detailed(params: &super::Params, status: &StatusCount) -> bool {
status.failed > 0
|| (status.neutral > 0 && params.show_skipped_rules)
|| (status.success > 0 && params.show_success_rules)
}

pub(super) struct MetricCheckTable<'a> {
params: &'a super::Params,
config: &'a Config,
values: &'a [MetricCheck],
}

impl<'e> MetricCheckTable<'e> {
pub fn new(params: &'e super::Params, config: &'e Config, values: &'e [MetricCheck]) -> Self {
Self {
params,
config,
values,
}
}

fn write_rule_check<'a, W: WriterExt>(
&self,
buf: Buffer<W, Body<'a>>,
check: &RuleCheck,
formatter: &human_number::Formatter<'_>,
) -> Buffer<W, Body<'a>> {
buf.cond(
check.status.is_failed()
|| (self.params.show_skipped_rules && check.status.is_skip())
|| (self.params.show_success_rules && check.status.is_success()),
|buf| {
buf.raw(check.status.emoji())
.raw(" ")
.raw(TextRule::new(formatter, &check.rule))
.node("br")
.close()
},
)
}

fn write_metric_check<'a, W: WriterExt>(
&self,
buf: Buffer<W, Body<'a>>,
check: &MetricCheck,
) -> Buffer<W, Body<'a>> {
let formatter = self.config.formatter(&check.diff.header.name);

let buf = buf.node("tr").content(|buf| {
buf.node("td")
.attr(("align", "center"))
.content(|buf| buf.raw(check.status.status().emoji()))
.node("td")
.attr(("align", "left"))
.content(|buf| buf.raw(TextMetricHeader::new(&check.diff.header)))
.node("td")
.attr(("align", "right"))
.content(|buf| {
buf.optional(check.diff.comparison.previous(), |buf, value| {
buf.raw(formatter.format(value))
})
})
.node("td")
.attr(("align", "right"))
.content(|buf| {
buf.optional(check.diff.comparison.current(), |buf, value| {
buf.raw(formatter.format(value))
})
})
.node("td")
.attr(("align", "right"))
.content(|buf| {
buf.optional(check.diff.comparison.delta(), |buf, delta| {
let buf = buf.raw(formatter.format(delta.absolute));
buf.optional(delta.relative, |buf, rel| {
buf.node("br")
.close()
.raw("(")
.raw(TextPercent::new(rel).with_sign(true))
.raw(")")
})
})
})
});

buf.cond(should_display_detailed(self.params, &check.status), |buf| {
buf.node("tr").content(|buf| {
buf.node("td")
.content(empty)
.node("td")
.attr(("colspan", "4"))
.content(|buf| {
let buf = check.checks.iter().fold(buf, |buf, rule_check| {
self.write_rule_check(buf, rule_check, &formatter)
});
check.subsets.iter().fold(buf, |buf, (title, subset)| {
buf.cond(
should_display_detailed(self.params, &subset.status),
|buf| {
let buf = buf
.node("i")
.content(|buf| buf.text(title))
.node("br")
.close();

subset.checks.iter().fold(buf, |buf, rule_check| {
self.write_rule_check(buf, rule_check, &formatter)
})
},
)
})
})
})
})
}

pub fn write<'a, W: WriterExt>(&self, buf: Buffer<W, Body<'a>>) -> Buffer<W, Body<'a>> {
buf.node("table").content(|buf| {
let buf = write_thead(buf);
buf.node("tbody").content(|buf| {
self.values
.iter()
.fold(buf, |buf, check| self.write_metric_check(buf, check))
})
})
}

pub fn render<W: std::io::Write>(&self, writer: W) -> W {
let buf = Buffer::from(writer);
let buf = self.write(buf);
buf.into_inner()
}
}
153 changes: 153 additions & 0 deletions src/cmd/check/format/markdown.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
use crate::entity::check::CheckList;
use crate::entity::config::Config;

pub struct MarkdownFormatter<'a> {
params: &'a super::Params,
}

impl<'a> MarkdownFormatter<'a> {
pub fn new(params: &'a super::Params) -> Self {
Self { params }
}

pub fn format<W: std::io::Write>(
&self,
res: &CheckList,
config: &Config,
stdout: W,
) -> std::io::Result<W> {
Ok(super::html::MetricCheckTable::new(self.params, config, &res.list).render(stdout))
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::cmd::check::format::Params;
use crate::cmd::prelude::BasicWriter;
use crate::entity::check::{CheckList, MetricCheck, Status, SubsetCheck};
use crate::entity::config::{MetricConfig, Rule, Unit};
use crate::entity::difference::{Comparison, MetricDiff};
use crate::entity::metric::MetricHeader;

fn complete_checklist() -> CheckList {
CheckList::default()
.with_check(
MetricCheck::new(MetricDiff::new(
MetricHeader::new("first")
.with_tag("platform.os", "linux")
.with_tag("platform.arch", "amd64")
.with_tag("unit", "byte"),
Comparison::matching(10.0, 20.0),
))
.with_check(Rule::max(30.0), Status::Success)
.with_subset(
"show_not_increase_too_much",
SubsetCheck::default()
.with_matching("platform.os", "linux")
.with_check(Rule::max_relative_increase(0.2), Status::Failed),
),
)
.with_check(
MetricCheck::new(MetricDiff::new(
MetricHeader::new("first")
.with_tag("platform.os", "linux")
.with_tag("platform.arch", "arm64")
.with_tag("unit", "byte"),
Comparison::matching(10.0, 11.0),
))
.with_check(Rule::max(30.0), Status::Success)
.with_subset(
"show_not_increase_too_much",
SubsetCheck::default()
.with_matching("platform.os", "linux")
.with_check(Rule::max_relative_increase(0.2), Status::Success),
),
)
// metric not known in config
.with_check(MetricCheck::new(MetricDiff::new(
MetricHeader::new("unknown"),
Comparison::matching(42.0, 28.0),
)))
// metric without general rule
.with_check(
MetricCheck::new(MetricDiff::new(
MetricHeader::new("noglobal"),
Comparison::matching(42.0, 28.0),
))
.with_subset(
"show_pass",
SubsetCheck::default()
.with_matching("foo", "bar")
.with_check(Rule::max_relative_increase(0.2), Status::Skip),
),
)
// metric that doesn't change
.with_check(
MetricCheck::new(MetricDiff::new(
MetricHeader::new("nochange"),
Comparison::matching(10.0, 10.0),
))
.with_check(Rule::max(30.0), Status::Success),
)
// metric that doesn't change
.with_check(
MetricCheck::new(MetricDiff::new(
MetricHeader::new("with-unit"),
Comparison::matching(1024.0 * 1024.0 * 20.0, 1024.0 * 1024.0 * 25.0),
))
.with_check(Rule::max(1024.0 * 1024.0 * 30.0), Status::Success),
)
// with absolute change
.with_check(
MetricCheck::new(MetricDiff::new(
MetricHeader::new("with-change"),
Comparison::matching(1024.0 * 1024.0 * 20.0, 1024.0 * 1024.0 * 25.0),
))
.with_check(
Rule::max_absolute_increase(1024.0 * 1024.0 * 10.0),
Status::Success,
)
.with_check(
Rule::max_absolute_increase(1024.0 * 1024.0 * 2.0),
Status::Failed,
),
)
}

#[test]
fn should_format_to_text_by_default() {
let config = Config::default().with_metric(
"with-unit",
MetricConfig::default().with_unit(Unit::binary().with_suffix("B")),
);
let markdown_formatter = MarkdownFormatter::new(&Params {
show_success_rules: false,
show_skipped_rules: false,
});
let list = complete_checklist();
let mut writter = BasicWriter::from(Vec::<u8>::new());
markdown_formatter
.format(&list, &config, &mut writter)
.unwrap();
let stdout = writter.into_string();
similar_asserts::assert_eq!(stdout, include_str!("./format_md_by_default.md"));
}

#[test]
fn should_format_to_text_with_success_showed() {
let config = Config::default().with_metric(
"with-unit",
MetricConfig::default().with_unit(Unit::binary().with_suffix("B")),
);
let formatter = MarkdownFormatter::new(&Params {
show_success_rules: true,
show_skipped_rules: true,
});
let list = complete_checklist();
let mut writter = BasicWriter::from(Vec::<u8>::new());
formatter.format(&list, &config, &mut writter).unwrap();
let stdout = writter.into_string();
similar_asserts::assert_eq!(stdout, include_str!("./format_md_with_success_showed.md"));
}
}
Loading

0 comments on commit 7f29875

Please sign in to comment.